Ver Fonte

Merge branch 'master' into backend_plugins

* master: (48 commits)
  fix: unit test fixed
  prettier: change to single quoting
  ux: minor name change to search sections
  db: fix postgres regression when comparing boolean columns/values (#10303)
  dashboard: delete row improvements
  fix missing comma in documentation output example
  fix broken link (#10291)
  minor fixes and formatting after review
  dashfolders: use validation service for folder creation and dashboard import. #10197
  dashfolders: support creating new folder when moving dashboards. #10197
  dashfolders: support creating new folder when saving a dashboard. #10197
  dashfolders: support creating new folder in dashboard settings. #10197
  dashfolders: support creating new folder from the folder picker. #10197
  tech: ran prettier on all scss files
  tech: ran pretttier on all typescript files
  search: closes dash search when selecting current dashboard (#10285)
  fix: Original dashboard link from snapshot should be an a-tag, not a button (#10269) (#10283)
  dashboard: fixes #10262
  added new to new dahsboard and folder
  test: Update test with new component signature
  ...
Carl Bergquist há 8 anos atrás
pai
commit
8a7c455697
100 ficheiros alterados com 2819 adições e 2044 exclusões
  1. 2 0
      CHANGELOG.md
  2. 3 0
      README.md
  3. 20 0
      docs/sources/features/datasources/mysql.md
  4. 1 1
      docs/sources/features/panels/singlestat.md
  5. 2 2
      docs/sources/http_api/user.md
  6. 13 3
      package.json
  7. 1 1
      pkg/api/index.go
  8. 3 2
      pkg/middleware/org_redirect.go
  9. 91 66
      pkg/services/alerting/extractor.go
  10. 28 416
      pkg/services/alerting/extractor_test.go
  11. 4 0
      pkg/services/alerting/notifiers/pushover.go
  12. 63 0
      pkg/services/alerting/test-data/graphite-alert.json
  13. 282 0
      pkg/services/alerting/test-data/influxdb-alert.json
  14. 62 0
      pkg/services/alerting/test-data/panels-missing-id.json
  15. 60 0
      pkg/services/alerting/test-data/v5-dashboard.json
  16. 2 1
      pkg/services/sqlstore/dashboard.go
  17. 6 1
      pkg/services/sqlstore/dashboard_acl.go
  18. 1 1
      pkg/services/sqlstore/migrations/team_mig.go
  19. 4 4
      pkg/services/sqlstore/search_builder.go
  20. 1 1
      pkg/tsdb/cloudwatch/metric_find_query.go
  21. 67 45
      public/app/app.ts
  22. 1 1
      public/app/core/app_events.ts
  23. 60 29
      public/app/core/components/PageHeader/PageHeader.tsx
  24. 24 20
      public/app/core/components/code_editor/code_editor.ts
  25. 4 3
      public/app/core/components/colorpicker/spectrum_picker.ts
  26. 6 7
      public/app/core/components/dashboard_selector.ts
  27. 41 24
      public/app/core/components/form_dropdown/form_dropdown.ts
  28. 4 4
      public/app/core/components/gf_page.ts
  29. 87 21
      public/app/core/components/grafana_app.ts
  30. 32 26
      public/app/core/components/help/help.ts
  31. 10 11
      public/app/core/components/info_popover.ts
  32. 21 11
      public/app/core/components/json_explorer/helpers.ts
  33. 70 36
      public/app/core/components/json_explorer/json_explorer.ts
  34. 20 19
      public/app/core/components/jsontree/jsontree.ts
  35. 10 7
      public/app/core/components/layout_selector/layout_selector.ts
  36. 18 14
      public/app/core/components/manage_dashboards/manage_dashboards.html
  37. 56 21
      public/app/core/components/manage_dashboards/manage_dashboards.ts
  38. 8 10
      public/app/core/components/navbar/navbar.ts
  39. 5 3
      public/app/core/components/org_switcher.ts
  40. 3 4
      public/app/core/components/query_part/query_part.ts
  41. 35 23
      public/app/core/components/query_part/query_part_editor.ts
  42. 1 3
      public/app/core/components/scroll/scroll.ts
  43. 2 2
      public/app/core/components/search/search.html
  44. 43 21
      public/app/core/components/search/search.ts
  45. 1 1
      public/app/core/components/search/search_results.html
  46. 9 2
      public/app/core/components/search/search_results.ts
  47. 24 8
      public/app/core/components/sidemenu/sidemenu.ts
  48. 6 7
      public/app/core/components/switch.ts
  49. 17 10
      public/app/core/components/team_picker.ts
  50. 17 10
      public/app/core/components/user_picker.ts
  51. 32 32
      public/app/core/config.ts
  52. 0 1
      public/app/core/constants.ts
  53. 1 2
      public/app/core/controllers/error_ctrl.ts
  54. 23 13
      public/app/core/controllers/inspect_ctrl.ts
  55. 18 18
      public/app/core/controllers/invited_ctrl.ts
  56. 3 3
      public/app/core/controllers/json_editor_ctrl.ts
  57. 11 10
      public/app/core/controllers/login_ctrl.ts
  58. 15 10
      public/app/core/controllers/reset_password_ctrl.ts
  59. 18 20
      public/app/core/controllers/signup_ctrl.ts
  60. 41 41
      public/app/core/core.ts
  61. 1 3
      public/app/core/directives/array_join.ts
  62. 2 3
      public/app/core/directives/diff-view.ts
  63. 16 12
      public/app/core/directives/give_focus.ts
  64. 58 58
      public/app/core/directives/misc.ts
  65. 8 6
      public/app/core/directives/ng_model_on_blur.ts
  66. 9 5
      public/app/core/directives/rebuild_on_change.ts
  67. 94 36
      public/app/core/directives/tags.ts
  68. 9 11
      public/app/core/filters/filters.ts
  69. 20 16
      public/app/core/live/live_srv.ts
  70. 5 9
      public/app/core/mod_defs.d.ts
  71. 6 6
      public/app/core/nav_model_srv.ts
  72. 45 19
      public/app/core/profiler.ts
  73. 15 11
      public/app/core/routes/bundle_loader.ts
  74. 25 22
      public/app/core/routes/dashboard_loaders.ts
  75. 284 262
      public/app/core/routes/routes.ts
  76. 47 25
      public/app/core/services/alert_srv.ts
  77. 7 5
      public/app/core/services/analytics.ts
  78. 126 102
      public/app/core/services/backend_srv.ts
  79. 6 3
      public/app/core/services/context_srv.ts
  80. 24 18
      public/app/core/services/dynamic_directive_srv.ts
  81. 28 4
      public/app/core/services/global_event_srv.ts
  82. 3 3
      public/app/core/services/impression_srv.ts
  83. 28 27
      public/app/core/services/keybindingSrv.ts
  84. 39 19
      public/app/core/services/ng_react.ts
  85. 2 2
      public/app/core/services/popover_srv.ts
  86. 17 11
      public/app/core/services/search_srv.ts
  87. 1 2
      public/app/core/services/timer.ts
  88. 2 3
      public/app/core/services/util_srv.ts
  89. 21 11
      public/app/core/specs/backend_srv_specs.ts
  90. 38 20
      public/app/core/specs/datemath.jest.ts
  91. 9 9
      public/app/core/specs/emitter.jest.ts
  92. 12 12
      public/app/core/specs/flatten.jest.ts
  93. 23 0
      public/app/core/specs/global_event_srv.jest.ts
  94. 43 35
      public/app/core/specs/kbn.jest.ts
  95. 85 117
      public/app/core/specs/manage_dashboards.jest.ts
  96. 17 10
      public/app/core/specs/org_switcher.jest.ts
  97. 29 17
      public/app/core/specs/rangeutil.jest.ts
  98. 19 26
      public/app/core/specs/search.jest.ts
  99. 57 10
      public/app/core/specs/search_results.jest.ts
  100. 26 22
      public/app/core/specs/search_srv.jest.ts

+ 2 - 0
CHANGELOG.md

@@ -1,5 +1,7 @@
 # 5.0.0 (unreleased / master branch)
 
+Grafana v5.0 is going to be the biggest and most foundational release Grafana has ever had, coming with a ton of UX improvements, a new dashboard grid engine, dashboard folders, user teams and permissions. Checkout out this [video preview](https://www.youtube.com/watch?v=BC_YRNpqj5k) of Grafana v5.
+
 ### New Features
 - **Dashboards** Dashboard folders, [#1611](https://github.com/grafana/grafana/issues/1611)
 - **Teams** User groups (teams) implemented. Can be used in folder & dashboard permission list.

+ 3 - 0
README.md

@@ -9,6 +9,9 @@ Graphite, Elasticsearch, OpenTSDB, Prometheus and InfluxDB.
 
 ![](http://docs.grafana.org/assets/img/features/dashboard_ex1.png)
 
+## Grafana v5 Alpha Preview
+Grafana master is now v5.0 alpha. This is going to be the biggest and most foundational release Grafana has ever had, coming with a ton of UX improvements, a new dashboard grid engine, dashboard folders, user teams and permissions. Checkout out this [video preview](https://www.youtube.com/watch?v=BC_YRNpqj5k) of Grafana v5.
+
 ## Installation
 Head to [docs.grafana.org](http://docs.grafana.org/installation/) and [download](https://grafana.com/get)
 the latest release.

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

@@ -45,7 +45,14 @@ To simplify syntax and to allow for dynamic parts, like date range filters, the
 
 Macro example | Description
 ------------ | -------------
+*$__time(dateColumn)* | Will be replaced by an expression to convert to a UNIX timestamp and rename the column to `time_sec`. For example, *UNIX_TIMESTAMP(dateColumn) as time_sec*
 *$__timeFilter(dateColumn)* | Will be replaced by a time range filter using the specified column name. For example, *dateColumn > FROM_UNIXTIME(1494410783) AND dateColumn < FROM_UNIXTIME(1494497183)*
+*$__timeFrom()* | Will be replaced by the start of the currently active time selection. For example, *FROM_UNIXTIME(1494410783)*
+*$__timeTo()* | Will be replaced by the end of the currently active time selection. For example, *FROM_UNIXTIME(1494497183)*
+*$__timeGroup(dateColumn,'5m')* | Will be replaced by an expression usable in GROUP BY clause. For example, *cast(cast(UNIX_TIMESTAMP(dateColumn)/(300) as signed)*300 as signed) as time_sec,*
+*$__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*
 
 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.
 
@@ -99,6 +106,19 @@ GROUP BY metric1, UNIX_TIMESTAMP(time_date_time) DIV 300
 ORDER BY time_sec asc
 ```
 
+Example with $__timeGroup macro:
+
+```sql
+SELECT
+  $__timeGroup(time_date_time,'5m') as time_sec,
+  min(value_double) as value,
+  metric_name as metric
+FROM test_data
+WHERE $__timeFilter(time_date_time)
+GROUP BY 1, metric_name
+ORDER BY 1
+```
+
 Currently, there is no support for a dynamic group by time based on time range & panel width.
 This is something we plan to add.
 

+ 1 - 1
docs/sources/features/panels/singlestat.md

@@ -47,7 +47,7 @@ The coloring options of the Singlestat Panel config allow you to dynamically cha
 2. **Thresholds**: Change the background and value colors dynamically within the panel, depending on the Singlestat value. The threshold field accepts **2 comma-separated** values which represent 3 ranges that correspond to the three colors directly to the right. For example: if the thresholds are 70, 90 then the first color represents < 70, the second color represents between 70 and 90 and the third color represents > 90.
 3. **Colors**: Select a color and opacity
 4. **Value**: This checkbox applies the configured thresholds and colors to the summary stat.
-5. **Invert order**: This link toggles the threshold color order.</br>For example: Green, Orange, Red (<img class="no-shadow" src="/img/docs(v1/gyr.png">) will become Red, Orange, Green (<img class="no-shadow" src="/img/docs/v1/ryg.png">).
+5. **Invert order**: This link toggles the threshold color order.</br>For example: Green, Orange, Red (<img class="no-shadow" src="/img/docs/v1/gyr.png">) will become Red, Orange, Green (<img class="no-shadow" src="/img/docs/v1/ryg.png">).
 
 ### Spark Lines
 

+ 2 - 2
docs/sources/http_api/user.md

@@ -156,7 +156,7 @@ HTTP/1.1 200
 Content-Type: application/json
 
 {
-  "email": "user@mygraf.com"
+  "email": "user@mygraf.com",
   "name": "admin",
   "login": "admin",
   "theme": "light",
@@ -409,4 +409,4 @@ HTTP/1.1 200
 Content-Type: application/json
 
 {"message":"Dashboard unstarred"}
-```
+```

+ 13 - 3
package.json

@@ -65,7 +65,7 @@
     "karma-sinon": "^1.0.5",
     "karma-sourcemap-loader": "^0.3.7",
     "karma-webpack": "^2.0.4",
-    "lint-staged": "^4.2.3",
+    "lint-staged": "^6.0.0",
     "load-grunt-tasks": "3.5.2",
     "mocha": "^4.0.1",
     "ng-annotate-loader": "^0.6.1",
@@ -76,7 +76,7 @@
     "postcss-browser-reporter": "^0.5.0",
     "postcss-loader": "^2.0.6",
     "postcss-reporter": "^5.0.0",
-    "prettier": "1.7.3",
+    "prettier": "1.9.2",
     "react-test-renderer": "^16.0.0",
     "sass-lint": "^1.10.2",
     "sass-loader": "^6.0.6",
@@ -103,7 +103,17 @@
     "lint": "tslint -c tslint.json --project tsconfig.json --type-check",
     "karma": "node ./node_modules/grunt-cli/bin/grunt karma:dev",
     "jest": "node ./node_modules/jest-cli/bin/jest.js --notify --watch",
-    "precommit": "node ./node_modules/grunt-cli/bin/grunt precommit"
+    "precommit": "lint-staged && node ./node_modules/grunt-cli/bin/grunt precommit"
+  },
+  "lint-staged": {
+    "*.{ts,tsx}": [
+      "prettier --single-quote --trailing-comma es5 --write",
+      "git add"
+    ],
+    "*.scss": [
+      "prettier --single-quote --write",
+      "git add"
+    ]
   },
   "license": "Apache-2.0",
   "dependencies": {

+ 1 - 1
pkg/api/index.go

@@ -92,7 +92,7 @@ func setIndexViewData(c *middleware.Context) (*dtos.IndexViewData, error) {
 			Text: "Create",
 			Id:   "create",
 			Icon: "fa fa-fw fa-plus",
-			Url:  setting.AppSubUrl + "dashboard/new",
+			Url:  setting.AppSubUrl + "/dashboard/new",
 			Children: []*dtos.NavLink{
 				{Text: "Dashboard", Icon: "gicon gicon-dashboard-new", Url: setting.AppSubUrl + "/dashboard/new"},
 				{Text: "Folder", SubTitle: "Create a new folder to organize your dashboards", Id: "folder", Icon: "gicon gicon-folder-new", Url: setting.AppSubUrl + "/dashboards/folder/new"},

+ 3 - 2
pkg/middleware/org_redirect.go

@@ -4,6 +4,7 @@ import (
 	"fmt"
 	"net/http"
 	"strconv"
+	"strings"
 
 	"github.com/grafana/grafana/pkg/bus"
 	"github.com/grafana/grafana/pkg/models"
@@ -41,7 +42,7 @@ func OrgRedirect() macaron.Handler {
 			return
 		}
 
-		newUrl := setting.ToAbsUrl(fmt.Sprintf("%s?%s", c.Req.URL.Path, c.Req.URL.Query().Encode()))
-		c.Redirect(newUrl, 302)
+		newURL := setting.ToAbsUrl(fmt.Sprintf("%s?%s", strings.TrimPrefix(c.Req.URL.Path, "/"), c.Req.URL.Query().Encode()))
+		c.Redirect(newURL, 302)
 	}
 }

+ 91 - 66
pkg/services/alerting/extractor.go

@@ -69,95 +69,120 @@ func copyJson(in *simplejson.Json) (*simplejson.Json, error) {
 	return simplejson.NewJson(rawJson)
 }
 
-func (e *DashAlertExtractor) GetAlerts() ([]*m.Alert, error) {
-	e.log.Debug("GetAlerts")
+func (e *DashAlertExtractor) GetAlertFromPanels(jsonWithPanels *simplejson.Json) ([]*m.Alert, error) {
+	alerts := make([]*m.Alert, 0)
 
-	dashboardJson, err := copyJson(e.Dash.Data)
-	if err != nil {
-		return nil, err
-	}
+	for _, panelObj := range jsonWithPanels.Get("panels").MustArray() {
+		panel := simplejson.NewFromAny(panelObj)
+		jsonAlert, hasAlert := panel.CheckGet("alert")
 
-	alerts := make([]*m.Alert, 0)
-	for _, rowObj := range dashboardJson.Get("rows").MustArray() {
-		row := simplejson.NewFromAny(rowObj)
+		if !hasAlert {
+			continue
+		}
 
-		for _, panelObj := range row.Get("panels").MustArray() {
-			panel := simplejson.NewFromAny(panelObj)
-			jsonAlert, hasAlert := panel.CheckGet("alert")
+		panelId, err := panel.Get("id").Int64()
+		if err != nil {
+			return nil, fmt.Errorf("panel id is required. err %v", err)
+		}
 
-			if !hasAlert {
-				continue
-			}
+		// backward compatibility check, can be removed later
+		enabled, hasEnabled := jsonAlert.CheckGet("enabled")
+		if hasEnabled && enabled.MustBool() == false {
+			continue
+		}
 
-			panelId, err := panel.Get("id").Int64()
-			if err != nil {
-				return nil, fmt.Errorf("panel id is required. err %v", err)
-			}
+		frequency, err := getTimeDurationStringToSeconds(jsonAlert.Get("frequency").MustString())
+		if err != nil {
+			return nil, ValidationError{Reason: "Could not parse frequency"}
+		}
+
+		alert := &m.Alert{
+			DashboardId: e.Dash.Id,
+			OrgId:       e.OrgId,
+			PanelId:     panelId,
+			Id:          jsonAlert.Get("id").MustInt64(),
+			Name:        jsonAlert.Get("name").MustString(),
+			Handler:     jsonAlert.Get("handler").MustInt64(),
+			Message:     jsonAlert.Get("message").MustString(),
+			Frequency:   frequency,
+		}
+
+		for _, condition := range jsonAlert.Get("conditions").MustArray() {
+			jsonCondition := simplejson.NewFromAny(condition)
+
+			jsonQuery := jsonCondition.Get("query")
+			queryRefId := jsonQuery.Get("params").MustArray()[0].(string)
+			panelQuery := findPanelQueryByRefId(panel, queryRefId)
 
-			// backward compatibility check, can be removed later
-			enabled, hasEnabled := jsonAlert.CheckGet("enabled")
-			if hasEnabled && enabled.MustBool() == false {
-				continue
+			if panelQuery == nil {
+				reason := fmt.Sprintf("Alert on PanelId: %v refers to query(%s) that cannot be found", alert.PanelId, queryRefId)
+				return nil, ValidationError{Reason: reason}
 			}
 
-			frequency, err := getTimeDurationStringToSeconds(jsonAlert.Get("frequency").MustString())
-			if err != nil {
-				return nil, ValidationError{Reason: "Could not parse frequency"}
+			dsName := ""
+			if panelQuery.Get("datasource").MustString() != "" {
+				dsName = panelQuery.Get("datasource").MustString()
+			} else if panel.Get("datasource").MustString() != "" {
+				dsName = panel.Get("datasource").MustString()
 			}
 
-			alert := &m.Alert{
-				DashboardId: e.Dash.Id,
-				OrgId:       e.OrgId,
-				PanelId:     panelId,
-				Id:          jsonAlert.Get("id").MustInt64(),
-				Name:        jsonAlert.Get("name").MustString(),
-				Handler:     jsonAlert.Get("handler").MustInt64(),
-				Message:     jsonAlert.Get("message").MustString(),
-				Frequency:   frequency,
+			if datasource, err := e.lookupDatasourceId(dsName); err != nil {
+				return nil, err
+			} else {
+				jsonQuery.SetPath([]string{"datasourceId"}, datasource.Id)
 			}
 
-			for _, condition := range jsonAlert.Get("conditions").MustArray() {
-				jsonCondition := simplejson.NewFromAny(condition)
+			if interval, err := panel.Get("interval").String(); err == nil {
+				panelQuery.Set("interval", interval)
+			}
 
-				jsonQuery := jsonCondition.Get("query")
-				queryRefId := jsonQuery.Get("params").MustArray()[0].(string)
-				panelQuery := findPanelQueryByRefId(panel, queryRefId)
+			jsonQuery.Set("model", panelQuery.Interface())
+		}
 
-				if panelQuery == nil {
-					reason := fmt.Sprintf("Alert on PanelId: %v refers to query(%s) that cannot be found", alert.PanelId, queryRefId)
-					return nil, ValidationError{Reason: reason}
-				}
+		alert.Settings = jsonAlert
 
-				dsName := ""
-				if panelQuery.Get("datasource").MustString() != "" {
-					dsName = panelQuery.Get("datasource").MustString()
-				} else if panel.Get("datasource").MustString() != "" {
-					dsName = panel.Get("datasource").MustString()
-				}
+		// validate
+		_, err = NewRuleFromDBAlert(alert)
+		if err == nil && alert.ValidToSave() {
+			alerts = append(alerts, alert)
+		} else {
+			return nil, err
+		}
+	}
 
-				if datasource, err := e.lookupDatasourceId(dsName); err != nil {
-					return nil, err
-				} else {
-					jsonQuery.SetPath([]string{"datasourceId"}, datasource.Id)
-				}
+	return alerts, nil
+}
 
-				if interval, err := panel.Get("interval").String(); err == nil {
-					panelQuery.Set("interval", interval)
-				}
+func (e *DashAlertExtractor) GetAlerts() ([]*m.Alert, error) {
+	e.log.Debug("GetAlerts")
 
-				jsonQuery.Set("model", panelQuery.Interface())
-			}
+	dashboardJson, err := copyJson(e.Dash.Data)
+	if err != nil {
+		return nil, err
+	}
 
-			alert.Settings = jsonAlert
+	alerts := make([]*m.Alert, 0)
 
-			// validate
-			_, err = NewRuleFromDBAlert(alert)
-			if err == nil && alert.ValidToSave() {
-				alerts = append(alerts, alert)
-			} else {
+	// We extract alerts from rows to be backwards compatible
+	// with the old dashboard json model.
+	rows := dashboardJson.Get("rows").MustArray()
+	if len(rows) > 0 {
+		for _, rowObj := range rows {
+			row := simplejson.NewFromAny(rowObj)
+			a, err := e.GetAlertFromPanels(row)
+			if err != nil {
 				return nil, err
 			}
+
+			alerts = append(alerts, a...)
+		}
+	} else {
+		a, err := e.GetAlertFromPanels(dashboardJson)
+		if err != nil {
+			return nil, err
 		}
+
+		alerts = append(alerts, a...)
 	}
 
 	e.log.Debug("Extracted alerts from dashboard", "alertCount", len(alerts))

+ 28 - 416
pkg/services/alerting/extractor_test.go

@@ -1,12 +1,12 @@
 package alerting
 
 import (
+	"io/ioutil"
 	"testing"
 
 	"github.com/grafana/grafana/pkg/bus"
 	"github.com/grafana/grafana/pkg/components/simplejson"
 	m "github.com/grafana/grafana/pkg/models"
-	"github.com/grafana/grafana/pkg/setting"
 	. "github.com/smartystreets/goconvey/convey"
 )
 
@@ -18,10 +18,6 @@ func TestAlertRuleExtraction(t *testing.T) {
 			return &FakeCondition{}, nil
 		})
 
-		setting.NewConfigContext(&setting.CommandLineArgs{
-			HomePath: "../../../",
-		})
-
 		// mock data
 		defaultDs := &m.DataSource{Id: 12, OrgId: 1, Name: "I am default", IsDefault: true}
 		graphite2Ds := &m.DataSource{Id: 15, OrgId: 1, Name: "graphite2"}
@@ -45,70 +41,8 @@ func TestAlertRuleExtraction(t *testing.T) {
 			return nil
 		})
 
-		json := `
-      {
-        "id": 57,
-        "title": "Graphite 4",
-        "originalTitle": "Graphite 4",
-        "tags": ["graphite"],
-        "rows": [
-        {
-          "panels": [
-          {
-            "title": "Active desktop users",
-            "editable": true,
-            "type": "graph",
-            "id": 3,
-            "targets": [
-            {
-              "refId": "A",
-              "target": "aliasByNode(statsd.fakesite.counters.session_start.desktop.count, 4)"
-            }
-            ],
-            "datasource": null,
-            "alert": {
-              "name": "name1",
-              "message": "desc1",
-              "handler": 1,
-              "frequency": "60s",
-              "conditions": [
-              {
-                "type": "query",
-                "query": {"params": ["A", "5m", "now"]},
-                "reducer": {"type": "avg", "params": []},
-                "evaluator": {"type": ">", "params": [100]}
-              }
-              ]
-            }
-          },
-          {
-            "title": "Active mobile users",
-            "id": 4,
-            "targets": [
-              {"refId": "A", "target": ""},
-              {"refId": "B", "target": "aliasByNode(statsd.fakesite.counters.session_start.mobile.count, 4)"}
-            ],
-            "datasource": "graphite2",
-            "alert": {
-              "name": "name2",
-              "message": "desc2",
-              "handler": 0,
-              "frequency": "60s",
-              "severity": "warning",
-              "conditions": [
-              {
-                "type": "query",
-                "query":  {"params": ["B", "5m", "now"]},
-                "reducer": {"type": "avg", "params": []},
-                "evaluator": {"type": ">", "params": [100]}
-              }
-              ]
-            }
-          }
-          ]
-        }
-      ]
-      }`
+		json, err := ioutil.ReadFile("./test-data/graphite-alert.json")
+		So(err, ShouldBeNil)
 
 		Convey("Extractor should not modify the original json", func() {
 			dashJson, err := simplejson.NewJson([]byte(json))
@@ -201,69 +135,8 @@ func TestAlertRuleExtraction(t *testing.T) {
 		})
 
 		Convey("Panels missing id should return error", func() {
-			panelWithoutId := `
-      {
-        "id": 57,
-        "title": "Graphite 4",
-        "originalTitle": "Graphite 4",
-        "tags": ["graphite"],
-        "rows": [
-        {
-          "panels": [
-          {
-            "title": "Active desktop users",
-            "editable": true,
-            "type": "graph",
-            "targets": [
-            {
-              "refId": "A",
-              "target": "aliasByNode(statsd.fakesite.counters.session_start.desktop.count, 4)"
-            }
-            ],
-            "datasource": null,
-            "alert": {
-              "name": "name1",
-              "message": "desc1",
-              "handler": 1,
-              "frequency": "60s",
-              "conditions": [
-              {
-                "type": "query",
-                "query": {"params": ["A", "5m", "now"]},
-                "reducer": {"type": "avg", "params": []},
-                "evaluator": {"type": ">", "params": [100]}
-              }
-              ]
-            }
-          },
-          {
-            "title": "Active mobile users",
-            "id": 4,
-            "targets": [
-              {"refId": "A", "target": ""},
-              {"refId": "B", "target": "aliasByNode(statsd.fakesite.counters.session_start.mobile.count, 4)"}
-            ],
-            "datasource": "graphite2",
-            "alert": {
-              "name": "name2",
-              "message": "desc2",
-              "handler": 0,
-              "frequency": "60s",
-              "severity": "warning",
-              "conditions": [
-              {
-                "type": "query",
-                "query":  {"params": ["B", "5m", "now"]},
-                "reducer": {"type": "avg", "params": []},
-                "evaluator": {"type": ">", "params": [100]}
-              }
-              ]
-            }
-          }
-          ]
-        }
-      ]
-			}`
+			panelWithoutId, err := ioutil.ReadFile("./test-data/panels-missing-id.json")
+			So(err, ShouldBeNil)
 
 			dashJson, err := simplejson.NewJson([]byte(panelWithoutId))
 			So(err, ShouldBeNil)
@@ -277,292 +150,31 @@ func TestAlertRuleExtraction(t *testing.T) {
 			})
 		})
 
+		Convey("Parse alerts from dashboard without rows", func() {
+			json, err := ioutil.ReadFile("./test-data/v5-dashboard.json")
+			So(err, ShouldBeNil)
+
+			dashJson, err := simplejson.NewJson(json)
+			So(err, ShouldBeNil)
+			dash := m.NewDashboardFromJson(dashJson)
+			extractor := NewDashAlertExtractor(dash, 1)
+
+			alerts, err := extractor.GetAlerts()
+
+			Convey("Get rules without error", func() {
+				So(err, ShouldBeNil)
+			})
+
+			Convey("Should have 2 alert rule", func() {
+				So(len(alerts), ShouldEqual, 2)
+			})
+		})
+
 		Convey("Parse and validate dashboard containing influxdb alert", func() {
+			json, err := ioutil.ReadFile("./test-data/influxdb-alert.json")
+			So(err, ShouldBeNil)
 
-			json2 := `{
-				  "id": 4,
-				  "title": "Influxdb",
-				  "tags": [
-				    "apa"
-				  ],
-				  "style": "dark",
-				  "timezone": "browser",
-				  "editable": true,
-				  "hideControls": false,
-				  "sharedCrosshair": false,
-				  "rows": [
-				    {
-				      "collapse": false,
-				      "editable": true,
-				      "height": "450px",
-				      "panels": [
-				        {
-				          "alert": {
-				            "conditions": [
-				              {
-				                "evaluator": {
-				                  "params": [
-				                    10
-				                  ],
-				                  "type": "gt"
-				                },
-				                "query": {
-				                  "params": [
-				                    "B",
-				                    "5m",
-				                    "now"
-				                  ]
-				                },
-				                "reducer": {
-				                  "params": [],
-				                  "type": "avg"
-				                },
-				                "type": "query"
-				              }
-				            ],
-				            "frequency": "3s",
-				            "handler": 1,
-				            "name": "Influxdb",
-				            "noDataState": "no_data",
-				            "notifications": [
-				              {
-				                "id": 6
-				              }
-				            ]
-				          },
-				          "alerting": {},
-				          "aliasColors": {
-				            "logins.count.count": "#890F02"
-				          },
-				          "bars": false,
-				          "datasource": "InfluxDB",
-				          "editable": true,
-				          "error": false,
-				          "fill": 1,
-				          "grid": {},
-				          "id": 1,
-				          "interval": ">10s",
-				          "isNew": true,
-				          "legend": {
-				            "avg": false,
-				            "current": false,
-				            "max": false,
-				            "min": false,
-				            "show": true,
-				            "total": false,
-				            "values": false
-				          },
-				          "lines": true,
-				          "linewidth": 2,
-				          "links": [],
-				          "nullPointMode": "connected",
-				          "percentage": false,
-				          "pointradius": 5,
-				          "points": false,
-				          "renderer": "flot",
-				          "seriesOverrides": [],
-				          "span": 10,
-				          "stack": false,
-				          "steppedLine": false,
-				          "targets": [
-				            {
-				              "groupBy": [
-				                {
-				                  "params": [
-				                    "$interval"
-				                  ],
-				                  "type": "time"
-				                },
-				                {
-				                  "params": [
-				                    "datacenter"
-				                  ],
-				                  "type": "tag"
-				                },
-				                {
-				                  "params": [
-				                    "none"
-				                  ],
-				                  "type": "fill"
-				                }
-				              ],
-				              "hide": false,
-				              "measurement": "logins.count",
-				              "policy": "default",
-				              "query": "SELECT 8 * count(\"value\") FROM \"logins.count\" WHERE $timeFilter GROUP BY time($interval), \"datacenter\" fill(none)",
-				              "rawQuery": true,
-				              "refId": "B",
-				              "resultFormat": "time_series",
-				              "select": [
-				                [
-				                  {
-				                    "params": [
-				                      "value"
-				                    ],
-				                    "type": "field"
-				                  },
-				                  {
-				                    "params": [],
-				                    "type": "count"
-				                  }
-				                ]
-				              ],
-				              "tags": []
-				            },
-				            {
-				              "groupBy": [
-				                {
-				                  "params": [
-				                    "$interval"
-				                  ],
-				                  "type": "time"
-				                },
-				                {
-				                  "params": [
-				                    "null"
-				                  ],
-				                  "type": "fill"
-				                }
-				              ],
-				              "hide": true,
-				              "measurement": "cpu",
-				              "policy": "default",
-				              "refId": "A",
-				              "resultFormat": "time_series",
-				              "select": [
-				                [
-				                  {
-				                    "params": [
-				                      "value"
-				                    ],
-				                    "type": "field"
-				                  },
-				                  {
-				                    "params": [],
-				                    "type": "mean"
-				                  }
-				                ],
-				                [
-				                  {
-				                    "params": [
-				                      "value"
-				                    ],
-				                    "type": "field"
-				                  },
-				                  {
-				                    "params": [],
-				                    "type": "sum"
-				                  }
-				                ]
-				              ],
-				              "tags": []
-				            }
-				          ],
-				          "thresholds": [
-				            {
-				              "colorMode": "critical",
-				              "fill": true,
-				              "line": true,
-				              "op": "gt",
-				              "value": 10
-				            }
-				          ],
-				          "timeFrom": null,
-				          "timeShift": null,
-				          "title": "Panel Title",
-				          "tooltip": {
-				            "msResolution": false,
-				            "ordering": "alphabetical",
-				            "shared": true,
-				            "sort": 0,
-				            "value_type": "cumulative"
-				          },
-				          "type": "graph",
-				          "xaxis": {
-				            "mode": "time",
-				            "name": null,
-				            "show": true,
-				            "values": []
-				          },
-				          "yaxes": [
-				            {
-				              "format": "short",
-				              "logBase": 1,
-				              "max": null,
-				              "min": null,
-				              "show": true
-				            },
-				            {
-				              "format": "short",
-				              "logBase": 1,
-				              "max": null,
-				              "min": null,
-				              "show": true
-				            }
-				          ]
-				        },
-				        {
-				          "editable": true,
-				          "error": false,
-				          "id": 2,
-				          "isNew": true,
-				          "limit": 10,
-				          "links": [],
-				          "show": "current",
-				          "span": 2,
-				          "stateFilter": [
-				            "alerting"
-				          ],
-				          "title": "Alert status",
-				          "type": "alertlist"
-				        }
-				      ],
-				      "title": "Row"
-				    }
-				  ],
-				  "time": {
-				    "from": "now-5m",
-				    "to": "now"
-				  },
-				  "timepicker": {
-				    "now": true,
-				    "refresh_intervals": [
-				      "5s",
-				      "10s",
-				      "30s",
-				      "1m",
-				      "5m",
-				      "15m",
-				      "30m",
-				      "1h",
-				      "2h",
-				      "1d"
-				    ],
-				    "time_options": [
-				      "5m",
-				      "15m",
-				      "1h",
-				      "6h",
-				      "12h",
-				      "24h",
-				      "2d",
-				      "7d",
-				      "30d"
-				    ]
-				  },
-				  "templating": {
-				    "list": []
-				  },
-				  "annotations": {
-				    "list": []
-				  },
-				  "schemaVersion": 13,
-				  "version": 120,
-				  "links": [],
-				  "gnetId": null
-				}`
-
-			dashJson, err := simplejson.NewJson([]byte(json2))
+			dashJson, err := simplejson.NewJson(json)
 			So(err, ShouldBeNil)
 			dash := m.NewDashboardFromJson(dashJson)
 			extractor := NewDashAlertExtractor(dash, 1)

+ 4 - 0
pkg/services/alerting/notifiers/pushover.go

@@ -133,6 +133,7 @@ func (this *PushoverNotifier) Notify(evalContext *alerting.EvalContext) error {
 		this.log.Error("Failed get rule link", "error", err)
 		return err
 	}
+
 	message := evalContext.Rule.Message
 	for idx, evt := range evalContext.EvalMatches {
 		message += fmt.Sprintf("\n<b>%s</b>: %v", evt.Metric, evt.Value)
@@ -146,6 +147,9 @@ func (this *PushoverNotifier) Notify(evalContext *alerting.EvalContext) error {
 	if evalContext.ImagePublicUrl != "" {
 		message += fmt.Sprintf("\n<a href=\"%s\">Show graph image</a>", evalContext.ImagePublicUrl)
 	}
+	if message == "" {
+		message = "Notification message missing (Set a notification message to replace this text.)"
+	}
 
 	q := url.Values{}
 	q.Add("user", this.UserKey)

+ 63 - 0
pkg/services/alerting/test-data/graphite-alert.json

@@ -0,0 +1,63 @@
+{
+    "id": 57,
+    "title": "Graphite 4",
+    "originalTitle": "Graphite 4",
+    "tags": ["graphite"],
+    "rows": [
+    {
+      "panels": [
+      {
+        "title": "Active desktop users",
+        "editable": true,
+        "type": "graph",
+        "id": 3,
+        "targets": [
+        {
+          "refId": "A",
+          "target": "aliasByNode(statsd.fakesite.counters.session_start.desktop.count, 4)"
+        }
+        ],
+        "datasource": null,
+        "alert": {
+          "name": "name1",
+          "message": "desc1",
+          "handler": 1,
+          "frequency": "60s",
+          "conditions": [
+          {
+            "type": "query",
+            "query": {"params": ["A", "5m", "now"]},
+            "reducer": {"type": "avg", "params": []},
+            "evaluator": {"type": ">", "params": [100]}
+          }
+          ]
+        }
+      },
+      {
+        "title": "Active mobile users",
+        "id": 4,
+        "targets": [
+          {"refId": "A", "target": ""},
+          {"refId": "B", "target": "aliasByNode(statsd.fakesite.counters.session_start.mobile.count, 4)"}
+        ],
+        "datasource": "graphite2",
+        "alert": {
+          "name": "name2",
+          "message": "desc2",
+          "handler": 0,
+          "frequency": "60s",
+          "severity": "warning",
+          "conditions": [
+          {
+            "type": "query",
+            "query":  {"params": ["B", "5m", "now"]},
+            "reducer": {"type": "avg", "params": []},
+            "evaluator": {"type": ">", "params": [100]}
+          }
+          ]
+        }
+      }
+      ]
+    }
+  ]
+  }

+ 282 - 0
pkg/services/alerting/test-data/influxdb-alert.json

@@ -0,0 +1,282 @@
+{
+    "id": 4,
+    "title": "Influxdb",
+    "tags": [
+      "apa"
+    ],
+    "style": "dark",
+    "timezone": "browser",
+    "editable": true,
+    "hideControls": false,
+    "sharedCrosshair": false,
+    "rows": [
+      {
+        "collapse": false,
+        "editable": true,
+        "height": "450px",
+        "panels": [
+          {
+            "alert": {
+              "conditions": [
+                {
+                  "evaluator": {
+                    "params": [
+                      10
+                    ],
+                    "type": "gt"
+                  },
+                  "query": {
+                    "params": [
+                      "B",
+                      "5m",
+                      "now"
+                    ]
+                  },
+                  "reducer": {
+                    "params": [],
+                    "type": "avg"
+                  },
+                  "type": "query"
+                }
+              ],
+              "frequency": "3s",
+              "handler": 1,
+              "name": "Influxdb",
+              "noDataState": "no_data",
+              "notifications": [
+                {
+                  "id": 6
+                }
+              ]
+            },
+            "alerting": {},
+            "aliasColors": {
+              "logins.count.count": "#890F02"
+            },
+            "bars": false,
+            "datasource": "InfluxDB",
+            "editable": true,
+            "error": false,
+            "fill": 1,
+            "grid": {},
+            "id": 1,
+            "interval": ">10s",
+            "isNew": true,
+            "legend": {
+              "avg": false,
+              "current": false,
+              "max": false,
+              "min": false,
+              "show": true,
+              "total": false,
+              "values": false
+            },
+            "lines": true,
+            "linewidth": 2,
+            "links": [],
+            "nullPointMode": "connected",
+            "percentage": false,
+            "pointradius": 5,
+            "points": false,
+            "renderer": "flot",
+            "seriesOverrides": [],
+            "span": 10,
+            "stack": false,
+            "steppedLine": false,
+            "targets": [
+              {
+                "groupBy": [
+                  {
+                    "params": [
+                      "$interval"
+                    ],
+                    "type": "time"
+                  },
+                  {
+                    "params": [
+                      "datacenter"
+                    ],
+                    "type": "tag"
+                  },
+                  {
+                    "params": [
+                      "none"
+                    ],
+                    "type": "fill"
+                  }
+                ],
+                "hide": false,
+                "measurement": "logins.count",
+                "policy": "default",
+                "query": "SELECT 8 * count(\"value\") FROM \"logins.count\" WHERE $timeFilter GROUP BY time($interval), \"datacenter\" fill(none)",
+                "rawQuery": true,
+                "refId": "B",
+                "resultFormat": "time_series",
+                "select": [
+                  [
+                    {
+                      "params": [
+                        "value"
+                      ],
+                      "type": "field"
+                    },
+                    {
+                      "params": [],
+                      "type": "count"
+                    }
+                  ]
+                ],
+                "tags": []
+              },
+              {
+                "groupBy": [
+                  {
+                    "params": [
+                      "$interval"
+                    ],
+                    "type": "time"
+                  },
+                  {
+                    "params": [
+                      "null"
+                    ],
+                    "type": "fill"
+                  }
+                ],
+                "hide": true,
+                "measurement": "cpu",
+                "policy": "default",
+                "refId": "A",
+                "resultFormat": "time_series",
+                "select": [
+                  [
+                    {
+                      "params": [
+                        "value"
+                      ],
+                      "type": "field"
+                    },
+                    {
+                      "params": [],
+                      "type": "mean"
+                    }
+                  ],
+                  [
+                    {
+                      "params": [
+                        "value"
+                      ],
+                      "type": "field"
+                    },
+                    {
+                      "params": [],
+                      "type": "sum"
+                    }
+                  ]
+                ],
+                "tags": []
+              }
+            ],
+            "thresholds": [
+              {
+                "colorMode": "critical",
+                "fill": true,
+                "line": true,
+                "op": "gt",
+                "value": 10
+              }
+            ],
+            "timeFrom": null,
+            "timeShift": null,
+            "title": "Panel Title",
+            "tooltip": {
+              "msResolution": false,
+              "ordering": "alphabetical",
+              "shared": true,
+              "sort": 0,
+              "value_type": "cumulative"
+            },
+            "type": "graph",
+            "xaxis": {
+              "mode": "time",
+              "name": null,
+              "show": true,
+              "values": []
+            },
+            "yaxes": [
+              {
+                "format": "short",
+                "logBase": 1,
+                "max": null,
+                "min": null,
+                "show": true
+              },
+              {
+                "format": "short",
+                "logBase": 1,
+                "max": null,
+                "min": null,
+                "show": true
+              }
+            ]
+          },
+          {
+            "editable": true,
+            "error": false,
+            "id": 2,
+            "isNew": true,
+            "limit": 10,
+            "links": [],
+            "show": "current",
+            "span": 2,
+            "stateFilter": [
+              "alerting"
+            ],
+            "title": "Alert status",
+            "type": "alertlist"
+          }
+        ],
+        "title": "Row"
+      }
+    ],
+    "time": {
+      "from": "now-5m",
+      "to": "now"
+    },
+    "timepicker": {
+      "now": true,
+      "refresh_intervals": [
+        "5s",
+        "10s",
+        "30s",
+        "1m",
+        "5m",
+        "15m",
+        "30m",
+        "1h",
+        "2h",
+        "1d"
+      ],
+      "time_options": [
+        "5m",
+        "15m",
+        "1h",
+        "6h",
+        "12h",
+        "24h",
+        "2d",
+        "7d",
+        "30d"
+      ]
+    },
+    "templating": {
+      "list": []
+    },
+    "annotations": {
+      "list": []
+    },
+    "schemaVersion": 13,
+    "version": 120,
+    "links": [],
+    "gnetId": null
+  }

+ 62 - 0
pkg/services/alerting/test-data/panels-missing-id.json

@@ -0,0 +1,62 @@
+{
+    "id": 57,
+    "title": "Graphite 4",
+    "originalTitle": "Graphite 4",
+    "tags": ["graphite"],
+    "rows": [
+    {
+      "panels": [
+      {
+        "title": "Active desktop users",
+        "editable": true,
+        "type": "graph",
+        "targets": [
+        {
+          "refId": "A",
+          "target": "aliasByNode(statsd.fakesite.counters.session_start.desktop.count, 4)"
+        }
+        ],
+        "datasource": null,
+        "alert": {
+          "name": "name1",
+          "message": "desc1",
+          "handler": 1,
+          "frequency": "60s",
+          "conditions": [
+          {
+            "type": "query",
+            "query": {"params": ["A", "5m", "now"]},
+            "reducer": {"type": "avg", "params": []},
+            "evaluator": {"type": ">", "params": [100]}
+          }
+          ]
+        }
+      },
+      {
+        "title": "Active mobile users",
+        "id": 4,
+        "targets": [
+          {"refId": "A", "target": ""},
+          {"refId": "B", "target": "aliasByNode(statsd.fakesite.counters.session_start.mobile.count, 4)"}
+        ],
+        "datasource": "graphite2",
+        "alert": {
+          "name": "name2",
+          "message": "desc2",
+          "handler": 0,
+          "frequency": "60s",
+          "severity": "warning",
+          "conditions": [
+          {
+            "type": "query",
+            "query":  {"params": ["B", "5m", "now"]},
+            "reducer": {"type": "avg", "params": []},
+            "evaluator": {"type": ">", "params": [100]}
+          }
+          ]
+        }
+      }
+      ]
+    }
+  ]
+        }

+ 60 - 0
pkg/services/alerting/test-data/v5-dashboard.json

@@ -0,0 +1,60 @@
+{
+    "id": 57,
+    "title": "Graphite 4",
+    "originalTitle": "Graphite 4",
+    "tags": ["graphite"],
+      "panels": [
+      {
+        "title": "Active desktop users",
+        "editable": true,
+        "type": "graph",
+        "id": 3,
+        "targets": [
+        {
+          "refId": "A",
+          "target": "aliasByNode(statsd.fakesite.counters.session_start.desktop.count, 4)"
+        }
+        ],
+        "datasource": null,
+        "alert": {
+          "name": "name1",
+          "message": "desc1",
+          "handler": 1,
+          "frequency": "60s",
+          "conditions": [
+          {
+            "type": "query",
+            "query": {"params": ["A", "5m", "now"]},
+            "reducer": {"type": "avg", "params": []},
+            "evaluator": {"type": ">", "params": [100]}
+          }
+          ]
+        }
+      },
+      {
+        "title": "Active mobile users",
+        "id": 4,
+        "targets": [
+          {"refId": "A", "target": ""},
+          {"refId": "B", "target": "aliasByNode(statsd.fakesite.counters.session_start.mobile.count, 4)"}
+        ],
+        "datasource": "graphite2",
+        "alert": {
+          "name": "name2",
+          "message": "desc2",
+          "handler": 0,
+          "frequency": "60s",
+          "severity": "warning",
+          "conditions": [
+          {
+            "type": "query",
+            "query":  {"params": ["B", "5m", "now"]},
+            "reducer": {"type": "avg", "params": []},
+            "evaluator": {"type": ">", "params": [100]}
+          }
+          ]
+        }
+
+    }
+  ]
+  }

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

@@ -345,8 +345,9 @@ func GetDashboards(query *m.GetDashboardsQuery) error {
 
 func GetDashboardsByPluginId(query *m.GetDashboardsByPluginIdQuery) error {
 	var dashboards = make([]*m.Dashboard, 0)
+	whereExpr := "org_id=? AND plugin_id=? AND is_folder=" + dialect.BooleanStr(false)
 
-	err := x.Where("org_id=? AND plugin_id=? AND is_folder=0", query.OrgId, query.PluginId).Find(&dashboards)
+	err := x.Where(whereExpr, query.OrgId, query.PluginId).Find(&dashboards)
 	query.Result = dashboards
 
 	if err != nil {

+ 6 - 1
pkg/services/sqlstore/dashboard_acl.go

@@ -170,7 +170,12 @@ func GetDashboardAclInfoList(query *m.GetDashboardAclInfoListQuery) error {
 			FROM dashboard_acl as da,
         dashboard as dash
         LEFT JOIN dashboard folder on dash.folder_id = folder.id
-			WHERE dash.id = ? AND (dash.has_acl = 0 or folder.has_acl = 0) AND da.dashboard_id = -1
+			WHERE
+				dash.id = ? AND (
+					dash.has_acl = ` + dialect.BooleanStr(false) + ` or
+					folder.has_acl = ` + dialect.BooleanStr(false) + `
+				) AND
+				da.dashboard_id = -1
 	`
 
 	query.Result = make([]*m.DashboardAclInfoDTO, 0)

+ 1 - 1
pkg/services/sqlstore/migrations/team_mig.go

@@ -7,7 +7,7 @@ func addTeamMigrations(mg *Migrator) {
 		Name: "team",
 		Columns: []*Column{
 			{Name: "id", Type: DB_BigInt, IsPrimaryKey: true, IsAutoIncrement: true},
-			{Name: "name", Type: DB_NVarchar, Length: 255, Nullable: false},
+			{Name: "name", Type: DB_NVarchar, Length: 190, Nullable: false},
 			{Name: "org_id", Type: DB_BigInt},
 			{Name: "created", Type: DB_DateTime, Nullable: false},
 			{Name: "updated", Type: DB_DateTime, Nullable: false},

+ 4 - 4
pkg/services/sqlstore/search_builder.go

@@ -175,14 +175,14 @@ func (sb *SearchBuilder) buildSearchWhereClause() {
 	}
 
 	if sb.signedInUser.OrgRole != m.ROLE_ADMIN {
-		allowedDashboardsSubQuery := ` AND (dashboard.has_acl = 0 OR dashboard.id in (
+		allowedDashboardsSubQuery := ` AND (dashboard.has_acl = ` + dialect.BooleanStr(false) + ` OR dashboard.id in (
 			SELECT distinct d.id AS DashboardId
 			FROM dashboard AS d
 	      		LEFT JOIN dashboard_acl as da on d.folder_id = da.dashboard_id or d.id = da.dashboard_id
 	      		LEFT JOIN team_member as ugm on ugm.team_id =  da.team_id
 	      		LEFT JOIN org_user ou on ou.role = da.role
 			WHERE
-			  d.has_acl = 1 and
+			  d.has_acl = ` + dialect.BooleanStr(true) + ` and
 				(da.user_id = ? or ugm.user_id = ? or ou.id is not null)
 			  and d.org_id = ?
 			)
@@ -198,11 +198,11 @@ func (sb *SearchBuilder) buildSearchWhereClause() {
 	}
 
 	if sb.whereTypeFolder {
-		sb.sql.WriteString(" AND dashboard.is_folder = 1")
+		sb.sql.WriteString(" AND dashboard.is_folder = " + dialect.BooleanStr(true))
 	}
 
 	if sb.whereTypeDash {
-		sb.sql.WriteString(" AND dashboard.is_folder = 0")
+		sb.sql.WriteString(" AND dashboard.is_folder = " + dialect.BooleanStr(false))
 	}
 
 	if len(sb.whereFolderIds) > 0 {

+ 1 - 1
pkg/tsdb/cloudwatch/metric_find_query.go

@@ -127,7 +127,7 @@ func init() {
 		"AWS/Events":           {"RuleName"},
 		"AWS/Firehose":         {"DeliveryStreamName"},
 		"AWS/IoT":              {"Protocol"},
-		"AWS/Kinesis":          {"StreamName", "ShardID"},
+		"AWS/Kinesis":          {"StreamName", "ShardId"},
 		"AWS/KinesisAnalytics": {"Flow", "Id", "Application"},
 		"AWS/Lambda":           {"FunctionName", "Resource", "Version", "Alias"},
 		"AWS/Logs":             {"LogGroupName", "DestinationType", "FilterName"},

+ 67 - 45
public/app/app.ts

@@ -21,12 +21,12 @@ import _ from 'lodash';
 import moment from 'moment';
 
 // add move to lodash for backward compatabiltiy
-_.move = function (array, fromIndex, toIndex) {
+_.move = function(array, fromIndex, toIndex) {
   array.splice(toIndex, 0, array.splice(fromIndex, 1)[0]);
   return array;
 };
 
-import {coreModule, registerAngularDirectives} from './core/core';
+import { coreModule, registerAngularDirectives } from './core/core';
 
 export class GrafanaApp {
   registerFunctions: any;
@@ -54,36 +54,49 @@ export class GrafanaApp {
 
     moment.locale(config.bootData.user.locale);
 
-    app.config(($locationProvider, $controllerProvider, $compileProvider, $filterProvider, $httpProvider, $provide) => {
-      // pre assing bindings before constructor calls
-      $compileProvider.preAssignBindingsEnabled(true);
-
-      if (config.buildInfo.env !== 'development') {
-        $compileProvider.debugInfoEnabled(false);
+    app.config(
+      (
+        $locationProvider,
+        $controllerProvider,
+        $compileProvider,
+        $filterProvider,
+        $httpProvider,
+        $provide
+      ) => {
+        // pre assing bindings before constructor calls
+        $compileProvider.preAssignBindingsEnabled(true);
+
+        if (config.buildInfo.env !== 'development') {
+          $compileProvider.debugInfoEnabled(false);
+        }
+
+        $httpProvider.useApplyAsync(true);
+
+        this.registerFunctions.controller = $controllerProvider.register;
+        this.registerFunctions.directive = $compileProvider.directive;
+        this.registerFunctions.factory = $provide.factory;
+        this.registerFunctions.service = $provide.service;
+        this.registerFunctions.filter = $filterProvider.register;
+
+        $provide.decorator('$http', [
+          '$delegate',
+          '$templateCache',
+          function($delegate, $templateCache) {
+            var get = $delegate.get;
+            $delegate.get = function(url, config) {
+              if (url.match(/\.html$/)) {
+                // some template's already exist in the cache
+                if (!$templateCache.get(url)) {
+                  url += '?v=' + new Date().getTime();
+                }
+              }
+              return get(url, config);
+            };
+            return $delegate;
+          },
+        ]);
       }
-
-      $httpProvider.useApplyAsync(true);
-
-      this.registerFunctions.controller = $controllerProvider.register;
-      this.registerFunctions.directive  = $compileProvider.directive;
-      this.registerFunctions.factory    = $provide.factory;
-      this.registerFunctions.service    = $provide.service;
-      this.registerFunctions.filter     = $filterProvider.register;
-
-      $provide.decorator("$http", ["$delegate", "$templateCache", function($delegate, $templateCache) {
-        var get = $delegate.get;
-        $delegate.get = function(url, config) {
-          if (url.match(/\.html$/)) {
-            // some template's already exist in the cache
-            if (!$templateCache.get(url)) {
-              url += "?v=" + new Date().getTime();
-            }
-          }
-          return get(url, config);
-        };
-        return $delegate;
-      }]);
-    });
+    );
 
     this.ngModuleDependencies = [
       'grafana.core',
@@ -95,10 +108,17 @@ export class GrafanaApp {
       'pasvaz.bindonce',
       'ui.bootstrap',
       'ui.bootstrap.tpls',
-      'react'
+      'react',
     ];
 
-    var module_types = ['controllers', 'directives', 'factories', 'services', 'filters', 'routes'];
+    var module_types = [
+      'controllers',
+      'directives',
+      'factories',
+      'services',
+      'filters',
+      'routes',
+    ];
 
     _.each(module_types, type => {
       var moduleName = 'grafana.' + type;
@@ -113,20 +133,22 @@ export class GrafanaApp {
 
     var preBootRequires = [System.import('app/features/all')];
 
-    Promise.all(preBootRequires).then(() => {
-      // disable tool tip animation
-      $.fn.tooltip.defaults.animation = false;
-      // bootstrap the app
-      angular.bootstrap(document, this.ngModuleDependencies).invoke(() => {
-        _.each(this.preBootModules, module => {
-          _.extend(module, this.registerFunctions);
+    Promise.all(preBootRequires)
+      .then(() => {
+        // disable tool tip animation
+        $.fn.tooltip.defaults.animation = false;
+        // bootstrap the app
+        angular.bootstrap(document, this.ngModuleDependencies).invoke(() => {
+          _.each(this.preBootModules, module => {
+            _.extend(module, this.registerFunctions);
+          });
+
+          this.preBootModules = null;
         });
-
-        this.preBootModules = null;
+      })
+      .catch(function(err) {
+        console.log('Application boot failed:', err);
       });
-    }).catch(function(err) {
-      console.log('Application boot failed:', err);
-    });
   }
 }
 

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

@@ -1,4 +1,4 @@
-import {Emitter} from './utils/emitter';
+import { Emitter } from './utils/emitter';
 
 var appEvents = new Emitter();
 export default appEvents;

+ 60 - 29
public/app/core/components/PageHeader/PageHeader.tsx

@@ -1,7 +1,7 @@
-import React from 'react';
-import { NavModel, NavModelItem } from '../../nav_model_srv';
-import classNames from 'classnames';
-import appEvents from 'app/core/app_events';
+import React from "react";
+import { NavModel, NavModelItem } from "../../nav_model_srv";
+import classNames from "classnames";
+import appEvents from "app/core/app_events";
 
 export interface IProps {
   model: NavModel;
@@ -9,12 +9,12 @@ export interface IProps {
 
 function TabItem(tab: NavModelItem) {
   if (tab.hideFromTabs) {
-    return (null);
+    return null;
   }
 
   let tabClasses = classNames({
-    'gf-tabs-link': true,
-    active: tab.active,
+    "gf-tabs-link": true,
+    active: tab.active
   });
 
   return (
@@ -28,8 +28,9 @@ function TabItem(tab: NavModelItem) {
 }
 
 function SelectOption(navItem: NavModelItem) {
-  if (navItem.hideFromTabs) { // TODO: Rename hideFromTabs => hideFromNav
-    return (null);
+  if (navItem.hideFromTabs) {
+    // TODO: Rename hideFromTabs => hideFromNav
+    return null;
   }
 
   return (
@@ -39,14 +40,22 @@ function SelectOption(navItem: NavModelItem) {
   );
 }
 
-function Navigation({main}: {main: NavModelItem}) {
-  return (<nav>
-    <SelectNav customCss="page-header__select_nav" main={main} />
-    <Tabs customCss="page-header__tabs" main={main} />
-  </nav>);
+function Navigation({ main }: { main: NavModelItem }) {
+  return (
+    <nav>
+      <SelectNav customCss="page-header__select-nav" main={main} />
+      <Tabs customCss="page-header__tabs" main={main} />
+    </nav>
+  );
 }
 
-function SelectNav({main, customCss}: {main: NavModelItem, customCss: string}) {
+function SelectNav({
+  main,
+  customCss
+}: {
+  main: NavModelItem;
+  customCss: string;
+}) {
   const defaultSelectedItem = main.children.find(navItem => {
     return navItem.active === true;
   });
@@ -54,17 +63,32 @@ function SelectNav({main, customCss}: {main: NavModelItem, customCss: string}) {
   const gotoUrl = evt => {
     var element = evt.target;
     var url = element.options[element.selectedIndex].value;
-    appEvents.emit('location-change', {href: url});
+    appEvents.emit("location-change", { href: url });
   };
 
-  return (<select
-    className={`gf-select-nav ${customCss}`}
-    defaultValue={defaultSelectedItem.url}
-    onChange={gotoUrl}>{main.children.map(SelectOption)}</select>);
+  return (
+    <div className={`gf-form-select-wrapper width-20 ${customCss}`}>
+      <label
+        className={`gf-form-select-icon ${defaultSelectedItem.icon}`}
+        htmlFor="page-header-select-nav"
+      />
+      {/* Label to make it clickable */}
+      <select
+        className="gf-select-nav gf-form-input"
+        defaultValue={defaultSelectedItem.url}
+        onChange={gotoUrl}
+        id="page-header-select-nav"
+      >
+        {main.children.map(SelectOption)}
+      </select>
+    </div>
+  );
 }
 
-function Tabs({main, customCss}: {main: NavModelItem, customCss: string}) {
-  return <ul className={`gf-tabs ${customCss}`}>{main.children.map(TabItem)}</ul>;
+function Tabs({ main, customCss }: { main: NavModelItem; customCss: string }) {
+  return (
+    <ul className={`gf-tabs ${customCss}`}>{main.children.map(TabItem)}</ul>
+  );
 }
 
 export default class PageHeader extends React.Component<IProps, any> {
@@ -77,7 +101,11 @@ export default class PageHeader extends React.Component<IProps, any> {
     for (let i = 0; i < breadcrumbs.length; i++) {
       const bc = breadcrumbs[i];
       if (bc.url) {
-        breadcrumbsResult.push(<a className="text-link" key={i} href={bc.url}>{bc.title}</a>);
+        breadcrumbsResult.push(
+          <a className="text-link" key={i} href={bc.url}>
+            {bc.title}
+          </a>
+        );
       } else {
         breadcrumbsResult.push(<span key={i}> / {bc.title}</span>);
       }
@@ -95,12 +123,15 @@ export default class PageHeader extends React.Component<IProps, any> {
 
         <div className="page-header__info-block">
           {main.text && <h1 className="page-header__title">{main.text}</h1>}
-          {main.breadcrumbs && main.breadcrumbs.length > 0 && (
-            <h1 className="page-header__title">
-              {this.renderBreadcrumb(main.breadcrumbs)}
-            </h1>)
-          }
-          {main.subTitle && <div className="page-header__sub-title">{main.subTitle}</div>}
+          {main.breadcrumbs &&
+            main.breadcrumbs.length > 0 && (
+              <h1 className="page-header__title">
+                {this.renderBreadcrumb(main.breadcrumbs)}
+              </h1>
+            )}
+          {main.subTitle && (
+            <div className="page-header__sub-title">{main.subTitle}</div>
+          )}
           {main.subType && (
             <div className="page-header__stamps">
               <i className={main.subType.icon} />

+ 24 - 20
public/app/core/components/code_editor/code_editor.ts

@@ -38,10 +38,12 @@ import 'brace/mode/sql';
 import 'brace/snippets/sql';
 import 'brace/mode/markdown';
 import 'brace/snippets/markdown';
+import 'brace/mode/json';
+import 'brace/snippets/json';
 
-const DEFAULT_THEME_DARK = "ace/theme/grafana-dark";
-const DEFAULT_THEME_LIGHT = "ace/theme/textmate";
-const DEFAULT_MODE = "text";
+const DEFAULT_THEME_DARK = 'ace/theme/grafana-dark';
+const DEFAULT_THEME_LIGHT = 'ace/theme/textmate';
+const DEFAULT_MODE = 'text';
 const DEFAULT_MAX_LINES = 10;
 const DEFAULT_TAB_SIZE = 2;
 const DEFAULT_BEHAVIOURS = true;
@@ -54,7 +56,9 @@ function link(scope, elem, attrs) {
   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 behavioursEnabled = attrs.behavioursEnabled
+    ? attrs.behavioursEnabled === 'true'
+    : DEFAULT_BEHAVIOURS;
 
   // Initialize editor
   let aceElem = elem.get(0);
@@ -68,7 +72,7 @@ function link(scope, elem, attrs) {
     behavioursEnabled: behavioursEnabled,
     highlightActiveLine: false,
     showPrintMargin: false,
-    autoScrollEditorIntoView: true // this is needed if editor is inside scrollable page
+    autoScrollEditorIntoView: true, // this is needed if editor is inside scrollable page
   };
 
   // Set options
@@ -84,12 +88,12 @@ function link(scope, elem, attrs) {
   setEditorContent(scope.content);
 
   // Add classes
-  elem.addClass("gf-code-editor");
-  let textarea = elem.find("textarea");
+  elem.addClass('gf-code-editor');
+  let textarea = elem.find('textarea');
   textarea.addClass('gf-form-input');
 
   if (scope.codeEditorFocus) {
-    setTimeout(function () {
+    setTimeout(function() {
       textarea.focus();
       var domEl = textarea[0];
       if (domEl.setSelectionRange) {
@@ -100,7 +104,7 @@ function link(scope, elem, attrs) {
   }
 
   // Event handlers
-  editorSession.on('change', (e) => {
+  editorSession.on('change', e => {
     scope.$apply(() => {
       let newValue = codeEditor.getValue();
       scope.content = newValue;
@@ -121,25 +125,25 @@ function link(scope, elem, attrs) {
     scope.onChange();
   });
 
-  scope.$on("$destroy", () => {
+  scope.$on('$destroy', () => {
     codeEditor.destroy();
   });
 
   // Keybindings
   codeEditor.commands.addCommand({
     name: 'executeQuery',
-    bindKey: {win: 'Ctrl-Enter', mac: 'Command-Enter'},
+    bindKey: { win: 'Ctrl-Enter', mac: 'Command-Enter' },
     exec: () => {
       scope.onChange();
-    }
+    },
   });
 
   function setLangMode(lang) {
-    ace.acequire("ace/ext/language_tools");
+    ace.acequire('ace/ext/language_tools');
     codeEditor.setOptions({
       enableBasicAutocompletion: true,
       enableLiveAutocompletion: true,
-      enableSnippets: true
+      enableSnippets: true,
     });
 
     if (scope.getCompleter()) {
@@ -173,13 +177,13 @@ export function codeEditorDirective() {
     restrict: 'E',
     template: editorTemplate,
     scope: {
-      content: "=",
-      datasource: "=",
-      codeEditorFocus: "<",
-      onChange: "&",
-      getCompleter: "&"
+      content: '=',
+      datasource: '=',
+      codeEditorFocus: '<',
+      onChange: '&',
+      getCompleter: '&',
     },
-    link: link
+    link: link,
   };
 }
 

+ 4 - 3
public/app/core/components/colorpicker/spectrum_picker.ts

@@ -12,13 +12,14 @@ export function spectrumPicker() {
     require: 'ngModel',
     scope: true,
     replace: true,
-    template: '<color-picker color="ngModel.$viewValue" onChange="onColorChange"></color-picker>',
+    template:
+      '<color-picker color="ngModel.$viewValue" onChange="onColorChange"></color-picker>',
     link: function(scope, element, attrs, ngModel) {
       scope.ngModel = ngModel;
-      scope.onColorChange = (color) => {
+      scope.onColorChange = color => {
         ngModel.$setViewValue(color);
       };
-    }
+    },
   };
 }
 coreModule.directive('spectrumPicker', spectrumPicker);

+ 6 - 7
public/app/core/components/dashboard_selector.ts

@@ -9,15 +9,14 @@ export class DashboardSelectorCtrl {
   options: any;
 
   /** @ngInject */
-  constructor(private backendSrv) {
-  }
+  constructor(private backendSrv) {}
 
   $onInit() {
-    this.options = [{value: 0, text: 'Default'}];
+    this.options = [{ value: 0, text: 'Default' }];
 
-    return this.backendSrv.search({starred: true}).then(res => {
+    return this.backendSrv.search({ starred: true }).then(res => {
       res.forEach(dash => {
-        this.options.push({value: dash.id, text: dash.title});
+        this.options.push({ value: dash.id, text: dash.title });
       });
     });
   }
@@ -31,8 +30,8 @@ export function dashboardSelector() {
     controllerAs: 'ctrl',
     template: template,
     scope: {
-      model: '='
-    }
+      model: '=',
+    },
   };
 }
 

+ 41 - 24
public/app/core/components/form_dropdown/form_dropdown.ts

@@ -4,8 +4,12 @@ import coreModule from '../../core_module';
 
 function typeaheadMatcher(item) {
   var str = this.query;
-  if (str[0] === '/') { str = str.substring(1); }
-  if (str[str.length - 1] === '/') { str = str.substring(0, str.length-1); }
+  if (str[0] === '/') {
+    str = str.substring(1);
+  }
+  if (str[str.length - 1] === '/') {
+    str = str.substring(0, str.length - 1);
+  }
   return item.toLowerCase().match(str.toLowerCase());
 }
 
@@ -28,19 +32,26 @@ export class FormDropdownCtrl {
   lookupText: boolean;
 
   /** @ngInject **/
-  constructor(private $scope, $element, private $sce, private templateSrv, private $q) {
+  constructor(
+    private $scope,
+    $element,
+    private $sce,
+    private templateSrv,
+    private $q
+  ) {
     this.inputElement = $element.find('input').first();
     this.linkElement = $element.find('a').first();
     this.linkMode = true;
     this.cancelBlur = null;
 
     // listen to model changes
-    $scope.$watch("ctrl.model", this.modelChanged.bind(this));
+    $scope.$watch('ctrl.model', this.modelChanged.bind(this));
 
     if (this.labelMode) {
       this.cssClasses = 'gf-form-label ' + this.cssClass;
     } else {
-      this.cssClasses = 'gf-form-input gf-form-input--dropdown ' + this.cssClass;
+      this.cssClasses =
+        'gf-form-input gf-form-input--dropdown ' + this.cssClass;
     }
 
     this.inputElement.attr('data-provide', 'typeahead');
@@ -55,7 +66,7 @@ export class FormDropdownCtrl {
     // modify typeahead lookup
     // this = typeahead
     var typeahead = this.inputElement.data('typeahead');
-    typeahead.lookup = function () {
+    typeahead.lookup = function() {
       this.query = this.$element.val() || '';
       var items = this.source(this.query, $.proxy(this.process, this));
       return items ? this.process(items) : items;
@@ -80,7 +91,7 @@ export class FormDropdownCtrl {
   }
 
   getOptionsInternal(query) {
-    var result = this.getOptions({$query: query});
+    var result = this.getOptions({ $query: query });
     if (this.isPromiseLike(result)) {
       return result;
     }
@@ -88,7 +99,7 @@ export class FormDropdownCtrl {
   }
 
   isPromiseLike(obj) {
-    return obj && (typeof obj.then === 'function');
+    return obj && typeof obj.then === 'function';
   }
 
   modelChanged() {
@@ -97,8 +108,8 @@ export class FormDropdownCtrl {
     } else {
       // if we have text use it
       if (this.lookupText) {
-        this.getOptionsInternal("").then(options => {
-          var item = _.find(options, {value: this.model});
+        this.getOptionsInternal('').then(options => {
+          var item = _.find(options, { value: this.model });
           this.updateDisplay(item ? item.text : this.model);
         });
       } else {
@@ -140,7 +151,9 @@ export class FormDropdownCtrl {
   }
 
   switchToLink(fromClick) {
-    if (this.linkMode && !fromClick) { return; }
+    if (this.linkMode && !fromClick) {
+      return;
+    }
 
     clearTimeout(this.cancelBlur);
     this.cancelBlur = null;
@@ -164,7 +177,7 @@ export class FormDropdownCtrl {
     }
 
     this.$scope.$apply(() => {
-      var option = _.find(this.optionCache, {text: text});
+      var option = _.find(this.optionCache, { text: text });
 
       if (option) {
         if (_.isObject(this.model)) {
@@ -186,20 +199,24 @@ export class FormDropdownCtrl {
       // property is synced with outerscope
       this.$scope.$$postDigest(() => {
         this.$scope.$apply(() => {
-          this.onChange({$option: option});
+          this.onChange({ $option: option });
         });
       });
-
     });
   }
 
   updateDisplay(text) {
     this.text = text;
-    this.display = this.$sce.trustAsHtml(this.templateSrv.highlightVariablesAsHtml(text));
+    this.display = this.$sce.trustAsHtml(
+      this.templateSrv.highlightVariablesAsHtml(text)
+    );
   }
 
   open() {
-    this.inputElement.css('width', (Math.max(this.linkElement.width(), 80) + 16) + 'px');
+    this.inputElement.css(
+      'width',
+      Math.max(this.linkElement.width(), 80) + 16 + 'px'
+    );
 
     this.inputElement.show();
     this.inputElement.focus();
@@ -215,7 +232,7 @@ export class FormDropdownCtrl {
   }
 }
 
-const template =  `
+const template = `
 <input type="text"
   data-provide="typeahead"
   class="gf-form-input"
@@ -238,13 +255,13 @@ export function formDropdownDirective() {
     bindToController: true,
     controllerAs: 'ctrl',
     scope: {
-      model: "=",
-      getOptions: "&",
-      onChange: "&",
-      cssClass: "@",
-      allowCustom: "@",
-      labelMode: "@",
-      lookupText: "@",
+      model: '=',
+      getOptions: '&',
+      onChange: '&',
+      cssClass: '@',
+      allowCustom: '@',
+      labelMode: '@',
+      lookupText: '@',
     },
   };
 }

+ 4 - 4
public/app/core/components/gf_page.ts

@@ -27,15 +27,15 @@ export function gfPageDirective() {
     restrict: 'E',
     template: template,
     scope: {
-      "model": "=",
+      model: '=',
     },
     transclude: {
-      'header': '?gfPageHeader',
-      'body': 'gfPageBody',
+      header: '?gfPageHeader',
+      body: 'gfPageBody',
     },
     link: function(scope, elem, attrs) {
       console.log(scope);
-    }
+    },
   };
 }
 

+ 87 - 21
public/app/core/components/grafana_app.ts

@@ -3,15 +3,21 @@ import _ from 'lodash';
 import $ from 'jquery';
 
 import coreModule from 'app/core/core_module';
-import {profiler} from 'app/core/profiler';
+import { profiler } from 'app/core/profiler';
 import appEvents from 'app/core/app_events';
 import Drop from 'tether-drop';
 
 export class GrafanaCtrl {
-
   /** @ngInject */
-  constructor($scope, alertSrv, utilSrv, $rootScope, $controller, contextSrv, globalEventSrv) {
-
+  constructor(
+    $scope,
+    alertSrv,
+    utilSrv,
+    $rootScope,
+    $controller,
+    contextSrv,
+    globalEventSrv
+  ) {
     $scope.init = function() {
       $scope.contextSrv = contextSrv;
 
@@ -27,7 +33,7 @@ export class GrafanaCtrl {
     };
 
     $scope.initDashboard = function(dashboardData, viewScope) {
-      $scope.appEvent("dashboard-fetch-end", dashboardData);
+      $scope.appEvent('dashboard-fetch-end', dashboardData);
       $controller('DashboardCtrl', { $scope: viewScope }).init(dashboardData);
     };
 
@@ -49,13 +55,62 @@ export class GrafanaCtrl {
     };
 
     $rootScope.colors = [
-      "#7EB26D","#EAB839","#6ED0E0","#EF843C","#E24D42","#1F78C1","#BA43A9","#705DA0",
-      "#508642","#CCA300","#447EBC","#C15C17","#890F02","#0A437C","#6D1F62","#584477",
-      "#B7DBAB","#F4D598","#70DBED","#F9BA8F","#F29191","#82B5D8","#E5A8E2","#AEA2E0",
-      "#629E51","#E5AC0E","#64B0C8","#E0752D","#BF1B00","#0A50A1","#962D82","#614D93",
-      "#9AC48A","#F2C96D","#65C5DB","#F9934E","#EA6460","#5195CE","#D683CE","#806EB7",
-      "#3F6833","#967302","#2F575E","#99440A","#58140C","#052B51","#511749","#3F2B5B",
-      "#E0F9D7","#FCEACA","#CFFAFF","#F9E2D2","#FCE2DE","#BADFF4","#F9D9F9","#DEDAF7"
+      '#7EB26D',
+      '#EAB839',
+      '#6ED0E0',
+      '#EF843C',
+      '#E24D42',
+      '#1F78C1',
+      '#BA43A9',
+      '#705DA0',
+      '#508642',
+      '#CCA300',
+      '#447EBC',
+      '#C15C17',
+      '#890F02',
+      '#0A437C',
+      '#6D1F62',
+      '#584477',
+      '#B7DBAB',
+      '#F4D598',
+      '#70DBED',
+      '#F9BA8F',
+      '#F29191',
+      '#82B5D8',
+      '#E5A8E2',
+      '#AEA2E0',
+      '#629E51',
+      '#E5AC0E',
+      '#64B0C8',
+      '#E0752D',
+      '#BF1B00',
+      '#0A50A1',
+      '#962D82',
+      '#614D93',
+      '#9AC48A',
+      '#F2C96D',
+      '#65C5DB',
+      '#F9934E',
+      '#EA6460',
+      '#5195CE',
+      '#D683CE',
+      '#806EB7',
+      '#3F6833',
+      '#967302',
+      '#2F575E',
+      '#99440A',
+      '#58140C',
+      '#052B51',
+      '#511749',
+      '#3F2B5B',
+      '#E0F9D7',
+      '#FCEACA',
+      '#CFFAFF',
+      '#F9E2D2',
+      '#FCE2DE',
+      '#BADFF4',
+      '#F9D9F9',
+      '#DEDAF7',
     ];
 
     $scope.init();
@@ -63,7 +118,12 @@ export class GrafanaCtrl {
 }
 
 /** @ngInject */
-export function grafanaAppDirective(playlistSrv, contextSrv, $timeout, $rootScope) {
+export function grafanaAppDirective(
+  playlistSrv,
+  contextSrv,
+  $timeout,
+  $rootScope
+) {
   return {
     restrict: 'E',
     controller: GrafanaCtrl,
@@ -92,7 +152,7 @@ export function grafanaAppDirective(playlistSrv, contextSrv, $timeout, $rootScop
       // tooltip removal fix
       // manage page classes
       var pageClass;
-      scope.$on("$routeChangeSuccess", function(evt, data) {
+      scope.$on('$routeChangeSuccess', function(evt, data) {
         if (pageClass) {
           body.removeClass(pageClass);
         }
@@ -107,7 +167,7 @@ export function grafanaAppDirective(playlistSrv, contextSrv, $timeout, $rootScop
         // clear body class sidemenu states
         body.removeClass('sidemenu-open--xs');
 
-        $("#tooltip, .tooltip").remove();
+        $('#tooltip, .tooltip').remove();
 
         // check for kiosk url param
         if (data.params.kiosk) {
@@ -140,7 +200,7 @@ export function grafanaAppDirective(playlistSrv, contextSrv, $timeout, $rootScop
           return;
         }
 
-        if ((new Date().getTime() - lastActivity) > inActiveTimeLimit) {
+        if (new Date().getTime() - lastActivity > inActiveTimeLimit) {
           activeUser = false;
           body.addClass('user-activity-low');
           // hide sidemenu
@@ -148,7 +208,7 @@ export function grafanaAppDirective(playlistSrv, contextSrv, $timeout, $rootScop
             sidemenuHidden = true;
             body.removeClass('sidemenu-open');
             $timeout(function() {
-              $rootScope.$broadcast("render");
+              $rootScope.$broadcast('render');
             }, 100);
           }
         }
@@ -165,7 +225,7 @@ export function grafanaAppDirective(playlistSrv, contextSrv, $timeout, $rootScop
             sidemenuHidden = false;
             body.addClass('sidemenu-open');
             $timeout(function() {
-              $rootScope.$broadcast("render");
+              $rootScope.$broadcast('render');
             }, 100);
           }
         }
@@ -209,7 +269,10 @@ export function grafanaAppDirective(playlistSrv, contextSrv, $timeout, $rootScop
 
         // hide search
         if (body.find('.search-container').length > 0) {
-          if (target.parents('.search-results-container, .search-field-wrapper').length === 0) {
+          if (
+            target.parents('.search-results-container, .search-field-wrapper')
+              .length === 0
+          ) {
             scope.$apply(function() {
               scope.appEvent('hide-dash-search');
             });
@@ -218,11 +281,14 @@ export function grafanaAppDirective(playlistSrv, contextSrv, $timeout, $rootScop
 
         // hide popovers
         var popover = elem.find('.popover');
-        if (popover.length > 0 && target.parents('.graph-legend').length === 0) {
+        if (
+          popover.length > 0 &&
+          target.parents('.graph-legend').length === 0
+        ) {
           popover.hide();
         }
       });
-    }
+    },
   };
 }
 

+ 32 - 26
public/app/core/components/help/help.ts

@@ -11,39 +11,45 @@ export class HelpCtrl {
   constructor() {
     this.tabIndex = 0;
     this.shortcuts = {
-      'Global': [
-        {keys: ['g', 'h'], description: 'Go to Home Dashboard'},
-        {keys: ['g', 'p'], description: 'Go to Profile'},
-        {keys: ['s', 'o'], description: 'Open search'},
-        {keys: ['s', 's'], description: 'Open search with starred filter'},
-        {keys: ['s', 't'], description: 'Open search in tags view'},
-        {keys: ['esc'], description: 'Exit edit/setting views'},
+      Global: [
+        { keys: ['g', 'h'], description: 'Go to Home Dashboard' },
+        { keys: ['g', 'p'], description: 'Go to Profile' },
+        { keys: ['s', 'o'], description: 'Open search' },
+        { keys: ['s', 's'], description: 'Open search with starred filter' },
+        { keys: ['s', 't'], description: 'Open search in tags view' },
+        { keys: ['esc'], description: 'Exit edit/setting views' },
       ],
-      'Dashboard': [
-        {keys: ['mod+s'], description: 'Save dashboard'},
-        {keys: ['mod+h'], description: 'Hide row controls'},
-        {keys: ['d', 'r'], description: 'Refresh all panels'},
-        {keys: ['d', 's'], description: 'Dashboard settings'},
-        {keys: ['d', 'v'], description: 'Toggle in-active / view mode'},
-        {keys: ['d', 'k'], description: 'Toggle kiosk mode (hides top nav)'},
-        {keys: ['d', 'E'], description: 'Expand all rows'},
-        {keys: ['d', 'C'], description: 'Collapse all rows'},
-        {keys: ['mod+o'], description: 'Toggle shared graph crosshair'},
+      Dashboard: [
+        { keys: ['mod+s'], description: 'Save dashboard' },
+        { keys: ['mod+h'], description: 'Hide row controls' },
+        { keys: ['d', 'r'], description: 'Refresh all panels' },
+        { keys: ['d', 's'], description: 'Dashboard settings' },
+        { keys: ['d', 'v'], description: 'Toggle in-active / view mode' },
+        { keys: ['d', 'k'], description: 'Toggle kiosk mode (hides top nav)' },
+        { keys: ['d', 'E'], description: 'Expand all rows' },
+        { keys: ['d', 'C'], description: 'Collapse all rows' },
+        { keys: ['mod+o'], description: 'Toggle shared graph crosshair' },
       ],
       'Focused Panel': [
-        {keys: ['e'], description: 'Toggle panel edit view'},
-        {keys: ['v'], description: 'Toggle panel fullscreen view'},
-        {keys: ['p', 's'], description: 'Open Panel Share Modal'},
-        {keys: ['p', 'r'], description: 'Remove Panel'},
+        { keys: ['e'], description: 'Toggle panel edit view' },
+        { keys: ['v'], description: 'Toggle panel fullscreen view' },
+        { keys: ['p', 's'], description: 'Open Panel Share Modal' },
+        { keys: ['p', 'r'], description: 'Remove Panel' },
       ],
       'Focused Row': [
-        {keys: ['r', 'c'], description: 'Collapse Row'},
-        {keys: ['r', 'r'], description: 'Remove Row'},
+        { keys: ['r', 'c'], description: 'Collapse Row' },
+        { keys: ['r', 'r'], description: 'Remove Row' },
       ],
       'Time Range': [
-        {keys: ['t', 'z'], description: 'Zoom out time range'},
-        {keys: ['t', '<i class="fa fa-long-arrow-left"></i>'], description: 'Move time range back'},
-        {keys: ['t', '<i class="fa fa-long-arrow-right"></i>'], description: 'Move time range forward'},
+        { keys: ['t', 'z'], description: 'Zoom out time range' },
+        {
+          keys: ['t', '<i class="fa fa-long-arrow-left"></i>'],
+          description: 'Move time range back',
+        },
+        {
+          keys: ['t', '<i class="fa fa-long-arrow-right"></i>'],
+          description: 'Move time range forward',
+        },
       ],
     };
   }

+ 10 - 11
public/app/core/components/info_popover.ts

@@ -26,10 +26,10 @@ export function infoPopover() {
       }
 
       transclude(function(clone, newScope) {
-        var content = document.createElement("div");
+        var content = document.createElement('div');
         content.className = 'markdown-html';
 
-        _.each(clone, (node) => {
+        _.each(clone, node => {
           content.appendChild(node);
         });
 
@@ -43,22 +43,21 @@ export function infoPopover() {
           tetherOptions: {
             offset: offset,
             constraints: [
-                {
-                  to: 'window',
-                  attachment: 'together',
-                  pin: true
-                }
-              ],
-          }
+              {
+                to: 'window',
+                attachment: 'together',
+                pin: true,
+              },
+            ],
+          },
         });
 
         var unbind = scope.$on('$destroy', function() {
           drop.destroy();
           unbind();
         });
-
       });
-    }
+    },
   };
 }
 

+ 21 - 11
public/app/core/components/json_explorer/helpers.ts

@@ -5,7 +5,7 @@
  * Escapes `"` charachters from string
 */
 function escapeString(str: string): string {
-  return str.replace('"', '\"');
+  return str.replace('"', '"');
 }
 
 /*
@@ -13,7 +13,7 @@ function escapeString(str: string): string {
 */
 export function isObject(value: any): boolean {
   var type = typeof value;
-  return !!value && (type === 'object');
+  return !!value && type === 'object';
 }
 
 /*
@@ -29,11 +29,11 @@ export function getObjectName(object: Object): string {
     return 'Object';
   }
   if (typeof object === 'object' && !object.constructor) {
-      return 'Object';
+    return 'Object';
   }
 
   const funcNameRegex = /function ([^(]*)/;
-  const results = (funcNameRegex).exec((object).constructor.toString());
+  const results = funcNameRegex.exec(object.constructor.toString());
   if (results && results.length > 1) {
     return results[1];
   } else {
@@ -45,27 +45,33 @@ export function getObjectName(object: Object): string {
  * Gets type of an object. Returns "null" for null objects
 */
 export function getType(object: Object): string {
-  if (object === null) { return 'null'; }
+  if (object === null) {
+    return 'null';
+  }
   return typeof object;
 }
 
 /*
  * Generates inline preview for a JavaScript object based on a value
 */
-export function getValuePreview (object: Object, value: string): string {
+export function getValuePreview(object: Object, value: string): string {
   var type = getType(object);
 
-  if (type === 'null' || type === 'undefined') { return type; }
+  if (type === 'null' || type === 'undefined') {
+    return type;
+  }
 
   if (type === 'string') {
     value = '"' + escapeString(value) + '"';
   }
   if (type === 'function') {
-
     // Remove content of the function
-    return object.toString()
+    return (
+      object
+        .toString()
         .replace(/[\r\n]/g, '')
-        .replace(/\{.*\}/, '') + '{…}';
+        .replace(/\{.*\}/, '') + '{…}'
+    );
   }
   return value;
 }
@@ -97,7 +103,11 @@ export function cssClass(className: string): string {
   * Creates a new DOM element wiht given type and class
   * TODO: move me to helpers
 */
-export function createElement(type: string, className?: string, content?: Element|string): Element {
+export function createElement(
+  type: string,
+  className?: string,
+  content?: Element | string
+): Element {
   const el = document.createElement(type);
   if (className) {
     el.classList.add(cssClass(className));

+ 70 - 36
public/app/core/components/json_explorer/json_explorer.ts

@@ -7,7 +7,7 @@ import {
   getType,
   getValuePreview,
   cssClass,
-  createElement
+  createElement,
 } from './helpers';
 
 import _ from 'lodash';
@@ -19,7 +19,12 @@ const JSON_DATE_REGEX = /\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z/;
 // When toggleing, don't animated removal or addition of more than a few items
 const MAX_ANIMATED_TOGGLE_ITEMS = 10;
 
-const requestAnimationFrame = window.requestAnimationFrame || function(cb: ()=>void) { cb(); return 0; };
+const requestAnimationFrame =
+  window.requestAnimationFrame ||
+  function(cb: () => void) {
+    cb();
+    return 0;
+  };
 
 export interface JsonExplorerConfig {
   animateOpen?: boolean;
@@ -30,18 +35,16 @@ export interface JsonExplorerConfig {
 const _defaultConfig: JsonExplorerConfig = {
   animateOpen: true,
   animateClose: true,
-  theme: null
+  theme: null,
 };
 
-
 /**
  * @class JsonExplorer
  *
  * JsonExplorer allows you to render JSON objects in HTML with a
  * **collapsible** navigation.
-*/
+ */
 export class JsonExplorer {
-
   // Hold the open state after the toggler is used
   private _isOpen: boolean = null;
 
@@ -77,9 +80,13 @@ export class JsonExplorer {
    *
    * @param {string} [key=undefined] The key that this object in it's parent
    * context
-  */
-  constructor(public json: any, private open = 1, private config: JsonExplorerConfig = _defaultConfig, private key?: string) {
-  }
+   */
+  constructor(
+    public json: any,
+    private open = 1,
+    private config: JsonExplorerConfig = _defaultConfig,
+    private key?: string
+  ) {}
 
   /*
    * is formatter open?
@@ -103,17 +110,19 @@ export class JsonExplorer {
    * is this a date string?
   */
   private get isDate(): boolean {
-    return (this.type === 'string') &&
+    return (
+      this.type === 'string' &&
       (DATE_STRING_REGEX.test(this.json) ||
-      JSON_DATE_REGEX.test(this.json) ||
-      PARTIAL_DATE_REGEX.test(this.json));
+        JSON_DATE_REGEX.test(this.json) ||
+        PARTIAL_DATE_REGEX.test(this.json))
+    );
   }
 
   /*
    * is this a URL string?
   */
   private get isUrl(): boolean {
-    return this.type === 'string' && (this.json.indexOf('http') === 0);
+    return this.type === 'string' && this.json.indexOf('http') === 0;
   }
 
   /*
@@ -142,7 +151,9 @@ export class JsonExplorer {
    * is this an empty object or array?
   */
   private get isEmpty(): boolean {
-    return this.isEmptyObject || (this.keys && !this.keys.length && this.isArray);
+    return (
+      this.isEmptyObject || (this.keys && !this.keys.length && this.isArray)
+    );
   }
 
   /*
@@ -174,7 +185,7 @@ export class JsonExplorer {
   */
   private get keys(): string[] {
     if (this.isObject) {
-      return Object.keys(this.json).map((key)=> key ? key : '""');
+      return Object.keys(this.json).map(key => (key ? key : '""'));
     } else {
       return [];
     }
@@ -183,7 +194,7 @@ export class JsonExplorer {
   /**
    * Toggles `isOpen` state
    *
-  */
+   */
   toggleOpen() {
     this.isOpen = !this.isOpen;
 
@@ -198,17 +209,17 @@ export class JsonExplorer {
   }
 
   /**
-  * Open all children up to a certain depth.
-  * Allows actions such as expand all/collapse all
-  *
-  */
+   * Open all children up to a certain depth.
+   * Allows actions such as expand all/collapse all
+   *
+   */
   openAtDepth(depth = 1) {
     if (depth < 0) {
       return;
     }
 
     this.open = depth;
-    this.isOpen = (depth !== 0);
+    this.isOpen = depth !== 0;
 
     if (this.element) {
       this.removeChildren(false);
@@ -223,8 +234,11 @@ export class JsonExplorer {
   }
 
   isNumberArray() {
-    return (this.json.length > 0 && this.json.length < 4) &&
-      (_.isNumber(this.json[0]) || _.isNumber(this.json[1]));
+    return (
+      this.json.length > 0 &&
+      this.json.length < 4 &&
+      (_.isNumber(this.json[0]) || _.isNumber(this.json[1]))
+    );
   }
 
   renderArray() {
@@ -235,13 +249,17 @@ export class JsonExplorer {
     if (this.isNumberArray()) {
       this.json.forEach((val, index) => {
         if (index > 0) {
-          arrayWrapperSpan.appendChild(createElement('span', 'array-comma', ','));
+          arrayWrapperSpan.appendChild(
+            createElement('span', 'array-comma', ',')
+          );
         }
         arrayWrapperSpan.appendChild(createElement('span', 'number', val));
       });
       this.skipChildren = true;
     } else {
-      arrayWrapperSpan.appendChild(createElement('span', 'number', (this.json.length)));
+      arrayWrapperSpan.appendChild(
+        createElement('span', 'number', this.json.length)
+      );
     }
 
     arrayWrapperSpan.appendChild(createElement('span', 'bracket', ']'));
@@ -280,7 +298,11 @@ export class JsonExplorer {
       const objectWrapperSpan = createElement('span');
 
       // get constructor name and append it to wrapper span
-      var constructorName = createElement('span', 'constructor-name', this.constructorName);
+      var constructorName = createElement(
+        'span',
+        'constructor-name',
+        this.constructorName
+      );
       objectWrapperSpan.appendChild(constructorName);
 
       // if it's an array append the array specific elements like brackets and length
@@ -294,7 +316,6 @@ export class JsonExplorer {
       togglerLink.appendChild(value);
       // Primitive values
     } else {
-
       // make a value holder element
       const value = this.isUrl ? createElement('a') : createElement('span');
 
@@ -366,17 +387,24 @@ export class JsonExplorer {
   /**
    * Appends all the children to children element
    * Animated option is used when user triggers this via a click
-  */
+   */
   appendChildren(animated = false) {
     const children = this.element.querySelector(`div.${cssClass('children')}`);
 
-    if (!children || this.isEmpty) { return; }
+    if (!children || this.isEmpty) {
+      return;
+    }
 
     if (animated) {
       let index = 0;
-      const addAChild = ()=> {
+      const addAChild = () => {
         const key = this.keys[index];
-        const formatter = new JsonExplorer(this.json[key], this.open - 1, this.config, key);
+        const formatter = new JsonExplorer(
+          this.json[key],
+          this.open - 1,
+          this.config,
+          key
+        );
         children.appendChild(formatter.render());
 
         index += 1;
@@ -391,10 +419,14 @@ export class JsonExplorer {
       };
 
       requestAnimationFrame(addAChild);
-
     } else {
       this.keys.forEach(key => {
-        const formatter = new JsonExplorer(this.json[key], this.open - 1, this.config, key);
+        const formatter = new JsonExplorer(
+          this.json[key],
+          this.open - 1,
+          this.config,
+          key
+        );
         children.appendChild(formatter.render());
       });
     }
@@ -403,13 +435,15 @@ export class JsonExplorer {
   /**
    * Removes all the children from children element
    * Animated option is used when user triggers this via a click
-  */
+   */
   removeChildren(animated = false) {
-    const childrenElement = this.element.querySelector(`div.${cssClass('children')}`) as HTMLDivElement;
+    const childrenElement = this.element.querySelector(
+      `div.${cssClass('children')}`
+    ) as HTMLDivElement;
 
     if (animated) {
       let childrenRemoved = 0;
-      const removeAChild = ()=> {
+      const removeAChild = () => {
         if (childrenElement && childrenElement.children.length) {
           childrenElement.removeChild(childrenElement.children[0]);
           childrenRemoved += 1;

+ 20 - 19
public/app/core/components/jsontree/jsontree.ts

@@ -1,22 +1,23 @@
 import coreModule from 'app/core/core_module';
-import {JsonExplorer} from '../json_explorer/json_explorer';
+import { JsonExplorer } from '../json_explorer/json_explorer';
 
-coreModule.directive('jsonTree', [function jsonTreeDirective() {
-  return{
-    restrict: 'E',
-    scope: {
-      object: '=',
-      startExpanded: '@',
-      rootName: '@',
-    },
-    link: function(scope, elem) {
+coreModule.directive('jsonTree', [
+  function jsonTreeDirective() {
+    return {
+      restrict: 'E',
+      scope: {
+        object: '=',
+        startExpanded: '@',
+        rootName: '@',
+      },
+      link: function(scope, elem) {
+        var jsonExp = new JsonExplorer(scope.object, 3, {
+          animateOpen: true,
+        });
 
-      var jsonExp = new JsonExplorer(scope.object, 3, {
-        animateOpen: true
-      });
-
-      const html = jsonExp.render(true);
-      elem.html(html);
-    }
-  };
-}]);
+        const html = jsonExp.render(true);
+        elem.html(html);
+      },
+    };
+  },
+]);

+ 10 - 7
public/app/core/components/layout_selector/layout_selector.ts

@@ -31,7 +31,6 @@ export class LayoutSelectorCtrl {
     store.set('grafana.list.layout.mode', 'grid');
     this.$rootScope.appEvent('layout-mode-changed', 'grid');
   }
-
 }
 
 /** @ngInject **/
@@ -56,12 +55,16 @@ export function layoutMode($rootScope) {
       var className = 'card-list-layout-' + layout;
       elem.addClass(className);
 
-      $rootScope.onAppEvent('layout-mode-changed', (evt, newLayout) => {
-        elem.removeClass(className);
-        className = 'card-list-layout-' + newLayout;
-        elem.addClass(className);
-      }, scope);
-    }
+      $rootScope.onAppEvent(
+        'layout-mode-changed',
+        (evt, newLayout) => {
+          elem.removeClass(className);
+          className = 'card-list-layout-' + newLayout;
+          elem.addClass(className);
+        },
+        scope
+      );
+    },
   };
 }
 

+ 18 - 14
public/app/core/components/manage_dashboards/manage_dashboards.html

@@ -60,20 +60,24 @@
         switch-class="gf-form-switch--transparent gf-form-switch--search-result-filter-row__checkbox"
       />
       <div class="search-results-filter-row__filters">
-        <select
-          class="search-results-filter-row__filters-item gf-form-input"
-          ng-model="ctrl.selectedStarredFilter"
-          ng-options="t.text disable when t.disabled for t in ctrl.starredFilterOptions"
-          ng-change="ctrl.onStarredFilterChange()"
-          ng-show="!(ctrl.canMove || ctrl.canDelete)"
-        />
-        <select
-          class="search-results-filter-row__filters-item gf-form-input"
-          ng-model="ctrl.selectedTagFilter"
-          ng-options="t.term disable when t.disabled for t in ctrl.tagFilterOptions"
-          ng-change="ctrl.onTagFilterChange()"
-          ng-show="!(ctrl.canMove || ctrl.canDelete)"
-        />
+        <div class="gf-form-select-wrapper">
+          <select
+            class="search-results-filter-row__filters-item gf-form-input"
+            ng-model="ctrl.selectedStarredFilter"
+            ng-options="t.text disable when t.disabled for t in ctrl.starredFilterOptions"
+            ng-change="ctrl.onStarredFilterChange()"
+            ng-show="!(ctrl.canMove || ctrl.canDelete)"
+          />
+        </div>
+        <div class="gf-form-select-wrapper">
+          <select
+            class="search-results-filter-row__filters-item gf-form-input"
+            ng-model="ctrl.selectedTagFilter"
+            ng-options="t.term disable when t.disabled for t in ctrl.tagFilterOptions"
+            ng-change="ctrl.onTagFilterChange()"
+            ng-show="!(ctrl.canMove || ctrl.canDelete)"
+          />
+        </div>
         <div class="gf-form-button-row" ng-show="ctrl.canMove || ctrl.canDelete">
           <button	type="button"
             class="btn gf-form-button btn-inverse"

+ 56 - 21
public/app/core/components/manage_dashboards/manage_dashboards.ts

@@ -13,13 +13,24 @@ export class ManageDashboardsCtrl {
   canMove = false;
   hasFilters = false;
   selectAllChecked = false;
-  starredFilterOptions = [{ text: 'Filter by Starred', disabled: true }, { text: 'Yes' }, { text: 'No' }];
+  starredFilterOptions = [
+    { text: 'Filter by Starred', disabled: true },
+    { text: 'Yes' },
+    { text: 'No' },
+  ];
   selectedStarredFilter: any;
   folderId?: number;
 
   /** @ngInject */
   constructor(private backendSrv, navModelSrv, private searchSrv: SearchSrv) {
-    this.query = { query: '', mode: 'tree', tag: [], starred: false, skipRecent: true, skipStarred: true };
+    this.query = {
+      query: '',
+      mode: 'tree',
+      tag: [],
+      starred: false,
+      skipRecent: true,
+      skipStarred: true,
+    };
 
     if (this.folderId) {
       this.query.folderIds = [this.folderId];
@@ -33,7 +44,7 @@ export class ManageDashboardsCtrl {
   }
 
   getDashboards() {
-    return this.searchSrv.search(this.query).then((result) => {
+    return this.searchSrv.search(this.query).then(result => {
       return this.initDashboardList(result);
     });
   }
@@ -42,7 +53,10 @@ export class ManageDashboardsCtrl {
     this.canMove = false;
     this.canDelete = false;
     this.selectAllChecked = false;
-    this.hasFilters = this.query.query.length > 0 || this.query.tag.length > 0 || this.query.starred;
+    this.hasFilters =
+      this.query.query.length > 0 ||
+      this.query.tag.length > 0 ||
+      this.query.starred;
 
     if (!result) {
       this.sections = [];
@@ -79,7 +93,7 @@ export class ManageDashboardsCtrl {
   getFoldersAndDashboardsToDelete() {
     let selectedDashboards = {
       folders: [],
-      dashboards: []
+      dashboards: [],
     };
 
     for (const section of this.sections) {
@@ -112,10 +126,16 @@ export class ManageDashboardsCtrl {
     let text2;
 
     if (folderCount > 0 && dashCount > 0) {
-      text += `selected folder${folderCount === 1 ? '' : 's'} and dashboard${dashCount === 1 ? '' : 's'}?`;
-      text2 = `All dashboards of the selected folder${folderCount === 1 ? '' : 's'} will also be deleted`;
+      text += `selected folder${folderCount === 1 ? '' : 's'} and dashboard${
+        dashCount === 1 ? '' : 's'
+      }?`;
+      text2 = `All dashboards of the selected folder${
+        folderCount === 1 ? '' : 's'
+      } will also be deleted`;
     } else if (folderCount > 0) {
-      text += `selected folder${folderCount === 1 ? '' : 's'} and all its dashboards?`;
+      text += `selected folder${
+        folderCount === 1 ? '' : 's'
+      } and all its dashboards?`;
     } else {
       text += `selected dashboard${dashCount === 1 ? '' : 's'}?`;
     }
@@ -129,7 +149,7 @@ export class ManageDashboardsCtrl {
       onConfirm: () => {
         const foldersAndDashboards = data.folders.concat(data.dashboards);
         this.deleteFoldersAndDashboards(foldersAndDashboards);
-      }
+      },
     });
   }
 
@@ -145,16 +165,22 @@ export class ManageDashboardsCtrl {
         let msg;
 
         if (folderCount > 0 && dashCount > 0) {
-          header = `Folder${folderCount === 1 ? '' : 's'} And Dashboard${dashCount === 1 ? '' : 's'} Deleted`;
+          header = `Folder${folderCount === 1 ? '' : 's'} And Dashboard${
+            dashCount === 1 ? '' : 's'
+          } Deleted`;
           msg = `${folderCount} folder${folderCount === 1 ? '' : 's'} `;
-          msg += `and ${dashCount} dashboard${dashCount === 1 ? '' : 's'} has been deleted`;
+          msg += `and ${dashCount} dashboard${
+            dashCount === 1 ? '' : 's'
+          } has been deleted`;
         } else if (folderCount > 0) {
           header = `Folder${folderCount === 1 ? '' : 's'} Deleted`;
 
           if (folderCount === 1) {
             msg = `${folders[0].dashboard.title} has been deleted`;
           } else {
-            msg = `${folderCount} folder${folderCount === 1 ? '' : 's'} has been deleted`;
+            msg = `${folderCount} folder${
+              folderCount === 1 ? '' : 's'
+            } has been deleted`;
           }
         } else if (dashCount > 0) {
           header = `Dashboard${dashCount === 1 ? '' : 's'} Deleted`;
@@ -162,7 +188,9 @@ export class ManageDashboardsCtrl {
           if (dashCount === 1) {
             msg = `${dashboards[0].dashboard.title} has been deleted`;
           } else {
-            msg = `${dashCount} dashboard${dashCount === 1 ? '' : 's'} has been deleted`;
+            msg = `${dashCount} dashboard${
+              dashCount === 1 ? '' : 's'
+            } has been deleted`;
           }
         }
 
@@ -187,19 +215,25 @@ export class ManageDashboardsCtrl {
   moveTo() {
     const selectedDashboards = this.getDashboardsToMove();
 
-    const template = '<move-to-folder-modal dismiss="dismiss()" ' +
+    const template =
+      '<move-to-folder-modal dismiss="dismiss()" ' +
       'dashboards="model.dashboards" after-save="model.afterSave()">' +
       '</move-to-folder-modal>`';
     appEvents.emit('show-modal', {
       templateHtml: template,
       modalClass: 'modal--narrow',
-      model: { dashboards: selectedDashboards, afterSave: this.getDashboards.bind(this) }
+      model: {
+        dashboards: selectedDashboards,
+        afterSave: this.getDashboards.bind(this),
+      },
     });
   }
 
   getTags() {
-    return this.searchSrv.getDashboardTags().then((results) => {
-      this.tagFilterOptions = [{ term: 'Filter By Tag', disabled: true }].concat(results);
+    return this.searchSrv.getDashboardTags().then(results => {
+      this.tagFilterOptions = [
+        { term: 'Filter By Tag', disabled: true },
+      ].concat(results);
       this.selectedTagFilter = this.tagFilterOptions[0];
     });
   }
@@ -248,7 +282,7 @@ export class ManageDashboardsCtrl {
         section.checked = this.selectAllChecked;
       }
 
-      section.items = _.map(section.items, (item) => {
+      section.items = _.map(section.items, item => {
         item.checked = this.selectAllChecked;
         return item;
       });
@@ -268,13 +302,14 @@ export class ManageDashboardsCtrl {
 export function manageDashboardsDirective() {
   return {
     restrict: 'E',
-    templateUrl: 'public/app/core/components/manage_dashboards/manage_dashboards.html',
+    templateUrl:
+      'public/app/core/components/manage_dashboards/manage_dashboards.html',
     controller: ManageDashboardsCtrl,
     bindToController: true,
     controllerAs: 'ctrl',
     scope: {
-      folderId: '='
-    }
+      folderId: '=',
+    },
   };
 }
 

+ 8 - 10
public/app/core/components/navbar/navbar.ts

@@ -1,15 +1,15 @@
 import coreModule from '../../core_module';
-import {NavModel}  from '../../nav_model_srv';
+import { NavModel } from '../../nav_model_srv';
+import appEvents from 'app/core/app_events';
 
 export class NavbarCtrl {
   model: NavModel;
 
   /** @ngInject */
-  constructor(private $rootScope) {
-  }
+  constructor() {}
 
   showSearch() {
-    this.$rootScope.appEvent('show-dash-search');
+    appEvents.emit('show-dash-search');
   }
 
   navItemClicked(navItem, evt) {
@@ -28,10 +28,9 @@ export function navbarDirective() {
     bindToController: true,
     controllerAs: 'ctrl',
     scope: {
-      model: "=",
+      model: '=',
     },
-    link: function(scope, elem) {
-    }
+    link: function(scope, elem) {},
   };
 }
 
@@ -46,11 +45,10 @@ export function pageH1() {
     </h1>
     `,
     scope: {
-      model: "=",
-    }
+      model: '=',
+    },
   };
 }
 
-
 coreModule.directive('pageH1', pageH1);
 coreModule.directive('navbar', navbarDirective);

+ 5 - 3
public/app/core/components/org_switcher.ts

@@ -1,7 +1,7 @@
 ///<reference path="../../headers/common.d.ts" />
 
 import coreModule from 'app/core/core_module';
-import {contextSrv} from 'app/core/services/context_srv';
+import { contextSrv } from 'app/core/services/context_srv';
 
 const template = `
 <div class="modal-body">
@@ -63,7 +63,9 @@ export class OrgSwitchCtrl {
   setUsingOrg(org) {
     return this.backendSrv.post('/api/user/using/' + org.orgId).then(() => {
       const re = /orgId=\d+/gi;
-      this.setWindowLocationHref(this.getWindowLocationHref().replace(re, 'orgId=' + org.orgId));
+      this.setWindowLocationHref(
+        this.getWindowLocationHref().replace(re, 'orgId=' + org.orgId)
+      );
     });
   }
 
@@ -83,7 +85,7 @@ export function orgSwitcher() {
     controller: OrgSwitchCtrl,
     bindToController: true,
     controllerAs: 'ctrl',
-    scope: {dismiss: "&"},
+    scope: { dismiss: '&' },
   };
 }
 

+ 3 - 4
public/app/core/components/query_part/query_part.ts

@@ -30,7 +30,7 @@ export class QueryPart {
     this.part = part;
     this.def = def;
     if (!this.def) {
-      throw {message: 'Could not find query part ' + part.type};
+      throw { message: 'Could not find query part ' + part.type };
     }
 
     part.params = part.params || _.clone(this.def.defaultParams);
@@ -42,7 +42,7 @@ export class QueryPart {
     return this.def.renderer(this, innerExpr);
   }
 
-  hasMultipleParamsInString (strValue, index) {
+  hasMultipleParamsInString(strValue, index) {
     if (strValue.indexOf(',') === -1) {
       return false;
     }
@@ -50,7 +50,7 @@ export class QueryPart {
     return this.def.params[index + 1] && this.def.params[index + 1].optional;
   }
 
-  updateParam (strValue, index) {
+  updateParam(strValue, index) {
     // handle optional parameters
     // if string contains ',' and next param is optional, split and update both
     if (this.hasMultipleParamsInString(strValue, index)) {
@@ -107,7 +107,6 @@ export function functionRenderer(part, innerExpr) {
   return str + parameters.join(', ') + ')';
 }
 
-
 export function suffixRenderer(part, innerExpr) {
   return innerExpr + ' ' + part.params[0];
 }

+ 35 - 23
public/app/core/components/query_part/query_part_editor.ts

@@ -15,17 +15,17 @@ var template = `
 </ul>
 `;
 
-  /** @ngInject */
+/** @ngInject */
 export function queryPartEditorDirective($compile, templateSrv) {
-
-  var paramTemplate = '<input type="text" class="hide input-mini tight-form-func-param"></input>';
+  var paramTemplate =
+    '<input type="text" class="hide input-mini tight-form-func-param"></input>';
 
   return {
     restrict: 'E',
     template: template,
     scope: {
-      part: "=",
-      handleEvent: "&",
+      part: '=',
+      handleEvent: '&',
     },
     link: function postLink($scope, elem) {
       var part = $scope.part;
@@ -40,7 +40,7 @@ export function queryPartEditorDirective($compile, templateSrv) {
         var $input = $link.next();
 
         $input.val(part.params[paramIndex]);
-        $input.css('width', ($link.width() + 16) + 'px');
+        $input.css('width', $link.width() + 16 + 'px');
 
         $link.hide();
         $input.show();
@@ -65,7 +65,7 @@ export function queryPartEditorDirective($compile, templateSrv) {
 
           part.updateParam($input.val(), paramIndex);
           $scope.$apply(() => {
-            $scope.handleEvent({$event: {name: 'part-param-changed'}});
+            $scope.handleEvent({ $event: { name: 'part-param-changed' } });
           });
         }
 
@@ -90,20 +90,26 @@ export function queryPartEditorDirective($compile, templateSrv) {
           return;
         }
 
-        var typeaheadSource = function (query, callback) {
+        var typeaheadSource = function(query, callback) {
           if (param.options) {
             var options = param.options;
             if (param.type === 'int') {
-              options = _.map(options, function(val) { return val.toString(); });
+              options = _.map(options, function(val) {
+                return val.toString();
+              });
             }
             return options;
           }
 
           $scope.$apply(function() {
-            $scope.handleEvent({$event: {name: 'get-param-options'}}).then(function(result) {
-              var dynamicOptions = _.map(result, function(op) { return op.value; });
-              callback(dynamicOptions);
-            });
+            $scope
+              .handleEvent({ $event: { name: 'get-param-options' } })
+              .then(function(result) {
+                var dynamicOptions = _.map(result, function(op) {
+                  return op.value;
+                });
+                callback(dynamicOptions);
+              });
           });
         };
 
@@ -113,16 +119,16 @@ export function queryPartEditorDirective($compile, templateSrv) {
           source: typeaheadSource,
           minLength: 0,
           items: 1000,
-          updater: function (value) {
+          updater: function(value) {
             setTimeout(function() {
               inputBlur.call($input[0], paramIndex);
             }, 0);
             return value;
-          }
+          },
         });
 
         var typeahead = $input.data('typeahead');
-        typeahead.lookup = function () {
+        typeahead.lookup = function() {
           this.query = this.$element.val() || '';
           var items = this.source(this.query, $.proxy(this.process, this));
           return items ? this.process(items) : items;
@@ -130,13 +136,15 @@ export function queryPartEditorDirective($compile, templateSrv) {
       }
 
       $scope.showActionsMenu = function() {
-        $scope.handleEvent({$event: {name: 'get-part-actions'}}).then(res => {
-          $scope.partActions = res;
-        });
+        $scope
+          .handleEvent({ $event: { name: 'get-part-actions' } })
+          .then(res => {
+            $scope.partActions = res;
+          });
       };
 
       $scope.triggerPartAction = function(action) {
-        $scope.handleEvent({$event: {name: 'action', action: action}});
+        $scope.handleEvent({ $event: { name: 'action', action: action } });
       };
 
       function addElementsAndCompile() {
@@ -149,8 +157,12 @@ export function queryPartEditorDirective($compile, templateSrv) {
             $('<span>, </span>').appendTo($paramsContainer);
           }
 
-          var paramValue = templateSrv.highlightVariablesAsHtml(part.params[index]);
-          var $paramLink = $('<a class="graphite-func-param-link pointer">' + paramValue + '</a>');
+          var paramValue = templateSrv.highlightVariablesAsHtml(
+            part.params[index]
+          );
+          var $paramLink = $(
+            '<a class="graphite-func-param-link pointer">' + paramValue + '</a>'
+          );
           var $input = $(paramTemplate);
 
           $paramLink.appendTo($paramsContainer);
@@ -171,7 +183,7 @@ export function queryPartEditorDirective($compile, templateSrv) {
       }
 
       relink();
-    }
+    },
   };
 }
 

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

@@ -5,7 +5,6 @@ export function geminiScrollbar() {
   return {
     restrict: 'A',
     link: function(scope, elem, attrs) {
-
       let scrollbar = new PerfectScrollbar(elem[0]);
 
       scope.$on('$routeChangeSuccess', () => {
@@ -19,8 +18,7 @@ export function geminiScrollbar() {
       scope.$on('$destroy', () => {
         scrollbar.destroy();
       });
-
-    }
+    },
   };
 }
 

+ 2 - 2
public/app/core/components/search/search.html

@@ -57,11 +57,11 @@
       <div class="search-filter-box">
         <a href="dashboard/new" class="search-filter-box-link">
           <i class="gicon gicon-dashboard-new"></i>
-          Dashboard
+          New dashboard
         </a>
         <a href="dashboards/folder/new" class="search-filter-box-link">
           <i class="gicon gicon-folder-new"></i>
-          Folder
+          New folder
         </a>
         <a class="search-filter-box-link" target="_blank" href="https://grafana.com/dashboards?utm_source=grafana_search">
           <img src="public/img/icn-dashboard-tiny.svg" width="20" /> Find  dashboards on Grafana.com

+ 43 - 21
public/app/core/components/search/search.ts

@@ -1,6 +1,7 @@
 import _ from 'lodash';
 import coreModule from '../../core_module';
 import { SearchSrv } from 'app/core/services/search_srv';
+import appEvents from 'app/core/app_events';
 
 export class SearchCtrl {
   isOpen: boolean;
@@ -16,11 +17,16 @@ export class SearchCtrl {
   initialFolderFilterTitle: string;
 
   /** @ngInject */
-  constructor($scope, private $location, private $timeout, private searchSrv: SearchSrv, $rootScope) {
-    $rootScope.onAppEvent('show-dash-search', this.openSearch.bind(this), $scope);
-    $rootScope.onAppEvent('hide-dash-search', this.closeSearch.bind(this), $scope);
-
-    this.initialFolderFilterTitle = "All";
+  constructor(
+    $scope,
+    private $location,
+    private $timeout,
+    private searchSrv: SearchSrv
+  ) {
+    appEvents.on('show-dash-search', this.openSearch.bind(this), $scope);
+    appEvents.on('hide-dash-search', this.closeSearch.bind(this), $scope);
+
+    this.initialFolderFilterTitle = 'All';
   }
 
   closeSearch() {
@@ -69,11 +75,14 @@ export class SearchCtrl {
 
       if (currentItem) {
         if (currentItem.dashboardIndex !== undefined) {
-          const selectedDash = this.results[currentItem.folderIndex].items[currentItem.dashboardIndex];
+          const selectedDash = this.results[currentItem.folderIndex].items[
+            currentItem.dashboardIndex
+          ];
 
           if (selectedDash) {
             this.$location.search({});
             this.$location.path(selectedDash.url);
+            this.closeSearch();
           }
         } else {
           const selectedFolder = this.results[currentItem.folderIndex];
@@ -96,7 +105,9 @@ export class SearchCtrl {
 
     if (currentItem) {
       if (currentItem.dashboardIndex !== undefined) {
-        this.results[currentItem.folderIndex].items[currentItem.dashboardIndex].selected = false;
+        this.results[currentItem.folderIndex].items[
+          currentItem.dashboardIndex
+        ].selected = false;
       } else {
         this.results[currentItem.folderIndex].selected = false;
       }
@@ -109,10 +120,13 @@ export class SearchCtrl {
 
     const max = flattenedResult.length;
     let newIndex = this.selectedIndex + direction;
-    this.selectedIndex = ((newIndex %= max) < 0) ? newIndex + max : newIndex;
+    this.selectedIndex = (newIndex %= max) < 0 ? newIndex + max : newIndex;
     const selectedItem = flattenedResult[this.selectedIndex];
 
-    if (selectedItem.dashboardIndex === undefined && this.results[selectedItem.folderIndex].id === 0) {
+    if (
+      selectedItem.dashboardIndex === undefined &&
+      this.results[selectedItem.folderIndex].id === 0
+    ) {
       this.moveSelection(direction);
       return;
     }
@@ -123,7 +137,9 @@ export class SearchCtrl {
         return;
       }
 
-      this.results[selectedItem.folderIndex].items[selectedItem.dashboardIndex].selected = true;
+      this.results[selectedItem.folderIndex].items[
+        selectedItem.dashboardIndex
+      ].selected = true;
       return;
     }
 
@@ -140,7 +156,9 @@ export class SearchCtrl {
     var localSearchId = this.currentSearchId;
 
     return this.searchSrv.search(this.query).then(results => {
-      if (localSearchId < this.currentSearchId) { return; }
+      if (localSearchId < this.currentSearchId) {
+        return;
+      }
       this.results = results || [];
       this.isLoading = false;
       this.moveSelection(1);
@@ -149,7 +167,9 @@ export class SearchCtrl {
 
   queryHasNoFilters() {
     var query = this.query;
-    return query.query === '' && query.starred === false && query.tag.length === 0;
+    return (
+      query.query === '' && query.starred === false && query.tag.length === 0
+    );
   }
 
   filterByTag(tag) {
@@ -169,7 +189,7 @@ export class SearchCtrl {
   }
 
   getTags() {
-    return this.searchSrv.getDashboardTags().then((results) => {
+    return this.searchSrv.getDashboardTags().then(results => {
       this.results = results;
       this.giveSearchFocus = this.giveSearchFocus + 1;
     });
@@ -194,21 +214,23 @@ export class SearchCtrl {
   private getFlattenedResultForNavigation() {
     let folderIndex = 0;
 
-    return _.flatMap(this.results, (s) => {
+    return _.flatMap(this.results, s => {
       let result = [];
 
       result.push({
-        folderIndex: folderIndex
+        folderIndex: folderIndex,
       });
 
       let dashboardIndex = 0;
 
-      result = result.concat(_.map(s.items || [], (i) => {
-        return {
-          folderIndex: folderIndex,
-          dashboardIndex: dashboardIndex++
-        };
-      }));
+      result = result.concat(
+        _.map(s.items || [], i => {
+          return {
+            folderIndex: folderIndex,
+            dashboardIndex: dashboardIndex++,
+          };
+        })
+      );
 
       folderIndex++;
       return result;

+ 1 - 1
public/app/core/components/search/search_results.html

@@ -32,7 +32,7 @@
       <span class="search-item__icon">
         <i class="gicon mini gicon-dashboard-list"></i>
       </span>
-      <span class="search-item__body">
+      <span class="search-item__body" ng-click="ctrl.onItemClick(item)">
         <div class="search-item__body-title">{{::item.title}}</div>
       </span>
       <span class="search-item__tags">

+ 9 - 2
public/app/core/components/search/search_results.ts

@@ -1,5 +1,6 @@
 import _ from 'lodash';
 import coreModule from '../../core_module';
+import appEvents from 'app/core/app_events';
 
 export class SearchResultsCtrl {
   results: any;
@@ -61,9 +62,15 @@ export class SearchResultsCtrl {
     }
   }
 
+  onItemClick(item) {
+    if (this.$location.path().indexOf(item.url) > -1) {
+      appEvents.emit('hide-dash-search');
+    }
+  }
+
   selectTag(tag, evt) {
     if (this.onTagSelected) {
-      this.onTagSelected({$tag: tag});
+      this.onTagSelected({ $tag: tag });
     }
 
     if (evt) {
@@ -85,7 +92,7 @@ export function searchResultsDirective() {
       results: '=',
       onSelectionChanged: '&',
       onTagSelected: '&',
-      onFolderExpanding: '&'
+      onFolderExpanding: '&',
     },
   };
 }

+ 24 - 8
public/app/core/components/sidemenu/sidemenu.ts

@@ -13,22 +13,36 @@ export class SideMenuCtrl {
   isOpenMobile: boolean;
 
   /** @ngInject */
-  constructor(private $scope, private $rootScope, private $location, private contextSrv, private $timeout) {
+  constructor(
+    private $scope,
+    private $rootScope,
+    private $location,
+    private contextSrv,
+    private $timeout
+  ) {
     this.isSignedIn = contextSrv.isSignedIn;
     this.user = contextSrv.user;
-    this.mainLinks = _.filter(config.bootData.navTree, item => !item.hideFromMenu);
-    this.bottomNav = _.filter(config.bootData.navTree, item => item.hideFromMenu);
-    this.loginUrl = 'login?redirect=' + encodeURIComponent(this.$location.path());
+    this.mainLinks = _.filter(
+      config.bootData.navTree,
+      item => !item.hideFromMenu
+    );
+    this.bottomNav = _.filter(
+      config.bootData.navTree,
+      item => item.hideFromMenu
+    );
+    this.loginUrl =
+      'login?redirect=' + encodeURIComponent(this.$location.path());
 
     if (contextSrv.user.orgCount > 1) {
-      let profileNode = _.find(this.bottomNav, {id: 'profile'});
+      let profileNode = _.find(this.bottomNav, { id: 'profile' });
       if (profileNode) {
         profileNode.showOrgSwitcher = true;
       }
     }
 
     this.$scope.$on('$routeChangeSuccess', () => {
-      this.loginUrl = 'login?redirect=' + encodeURIComponent(this.$location.path());
+      this.loginUrl =
+        'login?redirect=' + encodeURIComponent(this.$location.path());
     });
   }
 
@@ -53,7 +67,9 @@ export class SideMenuCtrl {
 
   itemClicked(item, evt) {
     if (item.url === '/shortcuts') {
-      appEvents.emit('show-modal', {templateHtml: '<help-modal></help-modal>'});
+      appEvents.emit('show-modal', {
+        templateHtml: '<help-modal></help-modal>',
+      });
       evt.preventDefault();
     }
   }
@@ -78,7 +94,7 @@ export function sideMenuDirective() {
           parent.append(menu);
         }, 100);
       });
-    }
+    },
   };
 }
 

+ 6 - 7
public/app/core/components/switch.ts

@@ -33,7 +33,6 @@ export class SwitchCtrl {
       return this.onChange();
     });
   }
-
 }
 
 export function switchDirective() {
@@ -43,12 +42,12 @@ export function switchDirective() {
     controllerAs: 'ctrl',
     bindToController: true,
     scope: {
-      checked: "=",
-      label: "@",
-      labelClass: "@",
-      tooltip: "@",
-      switchClass: "@",
-      onChange: "&",
+      checked: '=',
+      label: '@',
+      labelClass: '@',
+      tooltip: '@',
+      switchClass: '@',
+      onChange: '&',
     },
     template: template,
   };

+ 17 - 10
public/app/core/components/team_picker.ts

@@ -17,24 +17,31 @@ export class TeamPickerCtrl {
 
   /** @ngInject */
   constructor(private backendSrv) {
-    this.debouncedSearchGroups = _.debounce(this.searchGroups, 500, {'leading': true, 'trailing': false});
+    this.debouncedSearchGroups = _.debounce(this.searchGroups, 500, {
+      leading: true,
+      trailing: false,
+    });
     this.reset();
   }
 
   reset() {
-    this.group = {text: 'Choose', value: null};
+    this.group = { text: 'Choose', value: null };
   }
 
   searchGroups(query: string) {
-    return Promise.resolve(this.backendSrv.get('/api/teams/search?perpage=10&page=1&query=' + query).then(result => {
-      return _.map(result.teams, ug => {
-        return {text: ug.name, value: ug};
-      });
-    }));
+    return Promise.resolve(
+      this.backendSrv
+        .get('/api/teams/search?perpage=10&page=1&query=' + query)
+        .then(result => {
+          return _.map(result.teams, ug => {
+            return { text: ug.name, value: ug };
+          });
+        })
+    );
   }
 
   onChange(option) {
-    this.teamPicked({$group: option.value});
+    this.teamPicked({ $group: option.value });
   }
 }
 
@@ -49,10 +56,10 @@ export function teamPicker() {
       teamPicked: '&',
     },
     link: function(scope, elem, attrs, ctrl) {
-      scope.$on("team-picker-reset", () => {
+      scope.$on('team-picker-reset', () => {
         ctrl.reset();
       });
-    }
+    },
   };
 }
 

+ 17 - 10
public/app/core/components/user_picker.ts

@@ -18,23 +18,30 @@ export class UserPickerCtrl {
   /** @ngInject */
   constructor(private backendSrv) {
     this.reset();
-    this.debouncedSearchUsers = _.debounce(this.searchUsers, 500, {'leading': true, 'trailing': false});
+    this.debouncedSearchUsers = _.debounce(this.searchUsers, 500, {
+      leading: true,
+      trailing: false,
+    });
   }
 
   searchUsers(query: string) {
-    return Promise.resolve(this.backendSrv.get('/api/users/search?perpage=10&page=1&query=' + query).then(result => {
-      return _.map(result.users, user => {
-        return {text: user.login + ' -  ' + user.email, value: user};
-      });
-    }));
+    return Promise.resolve(
+      this.backendSrv
+        .get('/api/users/search?perpage=10&page=1&query=' + query)
+        .then(result => {
+          return _.map(result.users, user => {
+            return { text: user.login + ' -  ' + user.email, value: user };
+          });
+        })
+    );
   }
 
   onChange(option) {
-    this.userPicked({$user: option.value});
+    this.userPicked({ $user: option.value });
   }
 
   reset() {
-    this.user = {text: 'Choose', value: null};
+    this.user = { text: 'Choose', value: null };
   }
 }
 
@@ -56,10 +63,10 @@ export function userPicker() {
       userPicked: '&',
     },
     link: function(scope, elem, attrs, ctrl) {
-      scope.$on("user-picker-reset", () => {
+      scope.$on('user-picker-reset', () => {
         ctrl.reset();
       });
-    }
+    },
   };
 }
 

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

@@ -1,39 +1,39 @@
 import _ from 'lodash';
 
 class Settings {
-    datasources: any;
-    panels: any;
-    appSubUrl: string;
-    window_title_prefix: string;
-    buildInfo: any;
-    new_panel_title: string;
-    bootData: any;
-    externalUserMngLinkUrl: string;
-    externalUserMngLinkName: string;
-    externalUserMngInfo: string;
-    allowOrgCreate: boolean;
-    disableLoginForm: boolean;
-    defaultDatasource: string;
-    alertingEnabled: boolean;
-    authProxyEnabled: boolean;
-    ldapEnabled: boolean;
-    oauth: any;
-    disableUserSignUp: boolean;
-    loginHint: any;
-    loginError: any;
+  datasources: any;
+  panels: any;
+  appSubUrl: string;
+  window_title_prefix: string;
+  buildInfo: any;
+  new_panel_title: string;
+  bootData: any;
+  externalUserMngLinkUrl: string;
+  externalUserMngLinkName: string;
+  externalUserMngInfo: string;
+  allowOrgCreate: boolean;
+  disableLoginForm: boolean;
+  defaultDatasource: string;
+  alertingEnabled: boolean;
+  authProxyEnabled: boolean;
+  ldapEnabled: boolean;
+  oauth: any;
+  disableUserSignUp: boolean;
+  loginHint: any;
+  loginError: any;
 
-    constructor(options) {
-        var defaults = {
-            datasources: {},
-            window_title_prefix: 'Grafana - ',
-            panels: {},
-            new_panel_title: 'Panel Title',
-            playlist_timespan: "1m",
-            unsaved_changes_warning: true,
-            appSubUrl: ""
-        };
-         _.extend(this, defaults, options);
-    }
+  constructor(options) {
+    var defaults = {
+      datasources: {},
+      window_title_prefix: 'Grafana - ',
+      panels: {},
+      new_panel_title: 'Panel Title',
+      playlist_timespan: '1m',
+      unsaved_changes_warning: true,
+      appSubUrl: '',
+    };
+    _.extend(this, defaults, options);
+  }
 }
 
 var bootData = (<any>window).grafanaBootData || { settings: {} };

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

@@ -1,4 +1,3 @@
-
 export const GRID_CELL_HEIGHT = 30;
 export const GRID_CELL_VMARGIN = 10;
 export const GRID_COLUMN_COUNT = 24;

+ 1 - 2
public/app/core/controllers/error_ctrl.ts

@@ -3,7 +3,6 @@ import coreModule from '../core_module';
 import appEvents from 'app/core/app_events';
 
 export class ErrorCtrl {
-
   /** @ngInject */
   constructor($scope, contextSrv, navModelSrv) {
     $scope.navModel = navModelSrv.getNotFoundNav();
@@ -13,7 +12,7 @@ export class ErrorCtrl {
       appEvents.emit('toggle-sidemenu-hidden');
     }
 
-    $scope.$on("destroy", () => {
+    $scope.$on('destroy', () => {
       if (!contextSrv.isSignedIn) {
         appEvents.emit('toggle-sidemenu-hidden');
       }

+ 23 - 13
public/app/core/controllers/inspect_ctrl.ts

@@ -4,20 +4,19 @@ import $ from 'jquery';
 import coreModule from '../core_module';
 
 export class InspectCtrl {
-
   /** @ngInject */
   constructor($scope, $sanitize) {
     var model = $scope.inspector;
 
-    $scope.init = function () {
+    $scope.init = function() {
       $scope.editor = { index: 0 };
 
-      if (!model.error)  {
+      if (!model.error) {
         return;
       }
 
       if (_.isString(model.error.data)) {
-        $scope.response = $("<div>" + model.error.data + "</div>").text();
+        $scope.response = $('<div>' + model.error.data + '</div>').text();
       } else if (model.error.data) {
         if (model.error.data.response) {
           $scope.response = $sanitize(model.error.data.response);
@@ -29,8 +28,11 @@ export class InspectCtrl {
       }
 
       if (model.error.config && model.error.config.params) {
-        $scope.request_parameters = _.map(model.error.config.params, function(value, key) {
-          return { key: key, value: value};
+        $scope.request_parameters = _.map(model.error.config.params, function(
+          value,
+          key
+        ) {
+          return { key: key, value: value };
         });
       }
 
@@ -44,10 +46,15 @@ export class InspectCtrl {
         $scope.editor.index = 2;
 
         if (_.isString(model.error.config.data)) {
-          $scope.request_parameters = this.getParametersFromQueryString(model.error.config.data);
-        } else  {
-          $scope.request_parameters = _.map(model.error.config.data, function(value, key) {
-            return {key: key, value: angular.toJson(value, true)};
+          $scope.request_parameters = this.getParametersFromQueryString(
+            model.error.config.data
+          );
+        } else {
+          $scope.request_parameters = _.map(model.error.config.data, function(
+            value,
+            key
+          ) {
+            return { key: key, value: angular.toJson(value, true) };
           });
         }
       }
@@ -55,11 +62,14 @@ export class InspectCtrl {
   }
   getParametersFromQueryString(queryString) {
     var result = [];
-    var parameters = queryString.split("&");
+    var parameters = queryString.split('&');
     for (var i = 0; i < parameters.length; i++) {
-      var keyValue = parameters[i].split("=");
+      var keyValue = parameters[i].split('=');
       if (keyValue[1].length > 0) {
-        result.push({ key: keyValue[0], value: (<any>window).unescape(keyValue[1]) });
+        result.push({
+          key: keyValue[0],
+          value: (<any>window).unescape(keyValue[1]),
+        });
       }
     }
     return result;

+ 18 - 18
public/app/core/controllers/invited_ctrl.ts

@@ -2,7 +2,6 @@ import coreModule from '../core_module';
 import config from 'app/core/config';
 
 export class InvitedCtrl {
-
   /** @ngInject */
   constructor($scope, $routeParams, contextSrv, backendSrv) {
     contextSrv.sidemenu = false;
@@ -12,23 +11,22 @@ export class InvitedCtrl {
       main: {
         icon: 'gicon gicon-branding',
         subTitle: 'Register your Grafana account',
-        breadcrumbs: [
-          { title: 'Login', url: '/login' },
-          { title: 'Invite' },
-        ]
-      }
+        breadcrumbs: [{ title: 'Login', url: '/login' }, { title: 'Invite' }],
+      },
     };
 
     $scope.init = function() {
-      backendSrv.get('/api/user/invite/' + $routeParams.code).then(function(invite) {
-        $scope.formModel.name = invite.name;
-        $scope.formModel.email = invite.email;
-        $scope.formModel.username = invite.email;
-        $scope.formModel.inviteCode =  $routeParams.code;
-
-        $scope.greeting = invite.name || invite.email || invite.username;
-        $scope.invitedBy = invite.invitedBy;
-      });
+      backendSrv
+        .get('/api/user/invite/' + $routeParams.code)
+        .then(function(invite) {
+          $scope.formModel.name = invite.name;
+          $scope.formModel.email = invite.email;
+          $scope.formModel.username = invite.email;
+          $scope.formModel.inviteCode = $routeParams.code;
+
+          $scope.greeting = invite.name || invite.email || invite.username;
+          $scope.invitedBy = invite.invitedBy;
+        });
     };
 
     $scope.submit = function() {
@@ -36,9 +34,11 @@ export class InvitedCtrl {
         return;
       }
 
-      backendSrv.post('/api/user/invite/complete', $scope.formModel).then(function() {
-        window.location.href = config.appSubUrl + '/';
-      });
+      backendSrv
+        .post('/api/user/invite/complete', $scope.formModel)
+        .then(function() {
+          window.location.href = config.appSubUrl + '/';
+        });
     };
 
     $scope.init();

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

@@ -2,13 +2,13 @@ import angular from 'angular';
 import coreModule from '../core_module';
 
 export class JsonEditorCtrl {
-
   /** @ngInject */
   constructor($scope) {
     $scope.json = angular.toJson($scope.object, true);
-    $scope.canUpdate = $scope.updateHandler !== void 0 && $scope.contextSrv.isEditor;
+    $scope.canUpdate =
+      $scope.updateHandler !== void 0 && $scope.contextSrv.isEditor;
 
-    $scope.update = function () {
+    $scope.update = function() {
       var newObject = angular.fromJson($scope.json);
       $scope.updateHandler(newObject, $scope.object);
     };

+ 11 - 10
public/app/core/controllers/login_ctrl.ts

@@ -3,7 +3,6 @@ import coreModule from '../core_module';
 import config from 'app/core/config';
 
 export class LoginCtrl {
-
   /** @ngInject */
   constructor($scope, backendSrv, contextSrv, $location) {
     $scope.formModel = {
@@ -19,13 +18,13 @@ export class LoginCtrl {
 
     $scope.disableLoginForm = config.disableLoginForm;
     $scope.disableUserSignUp = config.disableUserSignUp;
-    $scope.loginHint     = config.loginHint;
+    $scope.loginHint = config.loginHint;
 
     $scope.loginMode = true;
     $scope.submitBtnText = 'Log in';
 
     $scope.init = function() {
-      $scope.$watch("loginMode", $scope.loginModeChanged);
+      $scope.$watch('loginMode', $scope.loginModeChanged);
 
       if (config.loginError) {
         $scope.appEvent('alert-warning', ['Login Failed', config.loginError]);
@@ -49,13 +48,15 @@ export class LoginCtrl {
         return;
       }
 
-      backendSrv.post('/api/user/signup', $scope.formModel).then(function(result) {
-        if (result.status === 'SignUpCreated') {
-          $location.path('/signup').search({email: $scope.formModel.email});
-        } else {
-          window.location.href = config.appSubUrl + '/';
-        }
-      });
+      backendSrv
+        .post('/api/user/signup', $scope.formModel)
+        .then(function(result) {
+          if (result.status === 'SignUpCreated') {
+            $location.path('/signup').search({ email: $scope.formModel.email });
+          } else {
+            window.location.href = config.appSubUrl + '/';
+          }
+        });
     };
 
     $scope.login = function() {

+ 15 - 10
public/app/core/controllers/reset_password_ctrl.ts

@@ -1,7 +1,6 @@
 import coreModule from '../core_module';
 
 export class ResetPasswordCtrl {
-
   /** @ngInject */
   constructor($scope, contextSrv, backendSrv, $location) {
     contextSrv.sidemenu = false;
@@ -21,30 +20,36 @@ export class ResetPasswordCtrl {
         breadcrumbs: [
           { title: 'Login', url: '/login' },
           { title: 'Reset Password' },
-        ]
-      }
+        ],
+      },
     };
 
     $scope.sendResetEmail = function() {
       if (!$scope.sendResetForm.$valid) {
         return;
       }
-      backendSrv.post('/api/user/password/send-reset-email', $scope.formModel).then(function() {
-        $scope.mode = 'email-sent';
-      });
+      backendSrv
+        .post('/api/user/password/send-reset-email', $scope.formModel)
+        .then(function() {
+          $scope.mode = 'email-sent';
+        });
     };
 
     $scope.submitReset = function() {
-      if (!$scope.resetForm.$valid) { return; }
+      if (!$scope.resetForm.$valid) {
+        return;
+      }
 
       if ($scope.formModel.newPassword !== $scope.formModel.confirmPassword) {
         $scope.appEvent('alert-warning', ['New passwords do not match', '']);
         return;
       }
 
-      backendSrv.post('/api/user/password/reset', $scope.formModel).then(function() {
-        $location.path('login');
-      });
+      backendSrv
+        .post('/api/user/password/reset', $scope.formModel)
+        .then(function() {
+          $location.path('login');
+        });
     };
   }
 }

+ 18 - 20
public/app/core/controllers/signup_ctrl.ts

@@ -4,14 +4,13 @@ import config from 'app/core/config';
 import coreModule from '../core_module';
 
 export class SignUpCtrl {
-
   /** @ngInject */
   constructor(
-      private $scope: any,
-      private backendSrv: any,
-      $location: any,
-      contextSrv: any) {
-
+    private $scope: any,
+    private backendSrv: any,
+    $location: any,
+    contextSrv: any
+  ) {
     contextSrv.sidemenu = false;
     $scope.ctrl = this;
 
@@ -30,11 +29,8 @@ export class SignUpCtrl {
       main: {
         icon: 'gicon gicon-branding',
         subTitle: 'Register your Grafana account',
-        breadcrumbs: [
-          { title: 'Login', url: '/login' },
-          { title: 'Sign Up' },
-        ]
-      }
+        breadcrumbs: [{ title: 'Login', url: '/login' }, { title: 'Sign Up' }],
+      },
     };
 
     backendSrv.get('/api/user/signup/options').then(options => {
@@ -43,20 +39,22 @@ export class SignUpCtrl {
     });
   }
 
-  submit () {
+  submit() {
     if (!this.$scope.signUpForm.$valid) {
       return;
     }
 
-    this.backendSrv.post('/api/user/signup/step2', this.$scope.formModel).then(rsp => {
-      if (rsp.code === 'redirect-to-select-org') {
-        window.location.href = config.appSubUrl + '/profile/select-org?signup=1';
-      } else {
-        window.location.href = config.appSubUrl + '/';
-      }
-    });
+    this.backendSrv
+      .post('/api/user/signup/step2', this.$scope.formModel)
+      .then(rsp => {
+        if (rsp.code === 'redirect-to-select-org') {
+          window.location.href =
+            config.appSubUrl + '/profile/select-org?signup=1';
+        } else {
+          window.location.href = config.appSubUrl + '/';
+        }
+      });
   }
 }
 
 coreModule.controller('SignUpCtrl', SignUpCtrl);
-

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

@@ -1,14 +1,14 @@
-import "./directives/dash_class";
-import "./directives/dash_edit_link";
-import "./directives/dropdown_typeahead";
-import "./directives/metric_segment";
-import "./directives/misc";
-import "./directives/ng_model_on_blur";
-import "./directives/tags";
-import "./directives/value_select_dropdown";
-import "./directives/rebuild_on_change";
-import "./directives/give_focus";
-import "./directives/diff-view";
+import './directives/dash_class';
+import './directives/dash_edit_link';
+import './directives/dropdown_typeahead';
+import './directives/metric_segment';
+import './directives/misc';
+import './directives/ng_model_on_blur';
+import './directives/tags';
+import './directives/value_select_dropdown';
+import './directives/rebuild_on_change';
+import './directives/give_focus';
+import './directives/diff-view';
 import './jquery_extended';
 import './partials';
 import './components/jsontree/jsontree';
@@ -20,19 +20,19 @@ import './components/colorpicker/spectrum_picker';
 import './services/search_srv';
 import './services/ng_react';
 
-import {grafanaAppDirective} from './components/grafana_app';
-import {sideMenuDirective} from './components/sidemenu/sidemenu';
-import {searchDirective} from './components/search/search';
-import {infoPopover} from './components/info_popover';
-import {navbarDirective} from './components/navbar/navbar';
-import {arrayJoin} from './directives/array_join';
-import {liveSrv} from './live/live_srv';
-import {Emitter} from './utils/emitter';
-import {layoutSelector} from './components/layout_selector/layout_selector';
-import {switchDirective} from './components/switch';
-import {dashboardSelector} from './components/dashboard_selector';
-import {queryPartEditorDirective} from './components/query_part/query_part_editor';
-import {formDropdownDirective} from './components/form_dropdown/form_dropdown';
+import { grafanaAppDirective } from './components/grafana_app';
+import { sideMenuDirective } from './components/sidemenu/sidemenu';
+import { searchDirective } from './components/search/search';
+import { infoPopover } from './components/info_popover';
+import { navbarDirective } from './components/navbar/navbar';
+import { arrayJoin } from './directives/array_join';
+import { liveSrv } from './live/live_srv';
+import { Emitter } from './utils/emitter';
+import { layoutSelector } from './components/layout_selector/layout_selector';
+import { switchDirective } from './components/switch';
+import { dashboardSelector } from './components/dashboard_selector';
+import { queryPartEditorDirective } from './components/query_part/query_part_editor';
+import { formDropdownDirective } from './components/form_dropdown/form_dropdown';
 import 'app/core/controllers/all';
 import 'app/core/services/all';
 import 'app/core/routes/routes';
@@ -40,23 +40,23 @@ import './filters/filters';
 import coreModule from './core_module';
 import appEvents from './app_events';
 import colors from './utils/colors';
-import {assignModelProperties} from './utils/model_utils';
-import {contextSrv} from './services/context_srv';
-import {KeybindingSrv} from './services/keybindingSrv';
-import {helpModal} from './components/help/help';
-import {JsonExplorer} from './components/json_explorer/json_explorer';
-import {NavModelSrv, NavModel} from './nav_model_srv';
-import {userPicker} from './components/user_picker';
-import {teamPicker} from './components/team_picker';
-import {geminiScrollbar} from './components/scroll/scroll';
-import {gfPageDirective} from './components/gf_page';
-import {orgSwitcher} from './components/org_switcher';
-import {profiler} from './profiler';
-import {registerAngularDirectives} from './angular_wrappers';
-import {updateLegendValues} from './time_series2';
+import { assignModelProperties } from './utils/model_utils';
+import { contextSrv } from './services/context_srv';
+import { KeybindingSrv } from './services/keybindingSrv';
+import { helpModal } from './components/help/help';
+import { JsonExplorer } from './components/json_explorer/json_explorer';
+import { NavModelSrv, NavModel } from './nav_model_srv';
+import { userPicker } from './components/user_picker';
+import { teamPicker } from './components/team_picker';
+import { geminiScrollbar } from './components/scroll/scroll';
+import { gfPageDirective } from './components/gf_page';
+import { orgSwitcher } from './components/org_switcher';
+import { profiler } from './profiler';
+import { registerAngularDirectives } from './angular_wrappers';
+import { updateLegendValues } from './time_series2';
 import TimeSeries from './time_series2';
-import {searchResultsDirective} from './components/search/search_results';
-import {manageDashboardsDirective} from './components/manage_dashboards/manage_dashboards';
+import { searchResultsDirective } from './components/search/search_results';
+import { manageDashboardsDirective } from './components/manage_dashboards/manage_dashboards';
 
 export {
   profiler,
@@ -92,5 +92,5 @@ export {
   manageDashboardsDirective,
   TimeSeries,
   updateLegendValues,
-  searchResultsDirective
+  searchResultsDirective,
 };

+ 1 - 3
public/app/core/directives/array_join.ts

@@ -10,7 +10,6 @@ export function arrayJoin() {
     restrict: 'A',
     require: 'ngModel',
     link: function(scope, element, attr, ngModel) {
-
       function split_array(text) {
         return (text || '').split(',');
       }
@@ -25,9 +24,8 @@ export function arrayJoin() {
 
       ngModel.$parsers.push(split_array);
       ngModel.$formatters.push(join_array);
-    }
+    },
   };
 }
 
 coreModule.directive('arrayJoin', arrayJoin);
-

+ 2 - 3
public/app/core/directives/diff-view.ts

@@ -8,8 +8,7 @@ export class DeltaCtrl {
 
   /** @ngInject */
   constructor(private $rootScope) {
-
-    const waitForCompile = (mutations) => {
+    const waitForCompile = mutations => {
       if (mutations.length === 1) {
         this.$rootScope.appEvent('json-diff-ready');
       }
@@ -72,7 +71,7 @@ export function linkJson() {
       link: '@lineLink',
       switchView: '&',
     },
-    template: `<a class="diff-linenum btn btn-inverse btn-small" ng-click="ctrl.goToLine(link)">Line {{ line }}</a>`
+    template: `<a class="diff-linenum btn btn-inverse btn-small" ng-click="ctrl.goToLine(link)">Line {{ line }}</a>`,
   };
 }
 coreModule.directive('diffLinkJson', linkJson);

+ 16 - 12
public/app/core/directives/give_focus.ts

@@ -8,19 +8,23 @@ coreModule.directive('giveFocus', function() {
       e.stopPropagation();
     });
 
-    scope.$watch(attrs.giveFocus, function (newValue) {
-      if (!newValue) {
-        return;
-      }
-      setTimeout(function() {
-        element.focus();
-        var domEl = element[0];
-        if (domEl.setSelectionRange) {
-          var pos = element.val().length * 2;
-          domEl.setSelectionRange(pos, pos);
+    scope.$watch(
+      attrs.giveFocus,
+      function(newValue) {
+        if (!newValue) {
+          return;
         }
-      }, 200);
-    }, true);
+        setTimeout(function() {
+          element.focus();
+          var domEl = element[0];
+          if (domEl.setSelectionRange) {
+            var pos = element.val().length * 2;
+            domEl.setSelectionRange(pos, pos);
+          }
+        }, 200);
+      },
+      true
+    );
   };
 });
 

+ 58 - 58
public/app/core/directives/misc.ts

@@ -1,50 +1,50 @@
-import angular from "angular";
-import Clipboard from "clipboard";
-import coreModule from "../core_module";
-import kbn from "app/core/utils/kbn";
+import angular from 'angular';
+import Clipboard from 'clipboard';
+import coreModule from '../core_module';
+import kbn from 'app/core/utils/kbn';
 
 /** @ngInject */
 function tip($compile) {
   return {
-    restrict: "E",
+    restrict: 'E',
     link: function(scope, elem, attrs) {
       var _t =
         '<i class="grafana-tip fa fa-' +
-        (attrs.icon || "question-circle") +
+        (attrs.icon || 'question-circle') +
         '" bs-tooltip="\'' +
         kbn.addslashes(elem.text()) +
-        "'\"></i>";
-      _t = _t.replace(/{/g, "\\{").replace(/}/g, "\\}");
+        '\'"></i>';
+      _t = _t.replace(/{/g, '\\{').replace(/}/g, '\\}');
       elem.replaceWith($compile(angular.element(_t))(scope));
-    }
+    },
   };
 }
 
 function clipboardButton() {
   return {
     scope: {
-      getText: "&clipboardButton"
+      getText: '&clipboardButton',
     },
     link: function(scope, elem) {
       scope.clipboard = new Clipboard(elem[0], {
         text: function() {
           return scope.getText();
-        }
+        },
       });
 
-      scope.$on("$destroy", function() {
+      scope.$on('$destroy', function() {
         if (scope.clipboard) {
           scope.clipboard.destroy();
         }
       });
-    }
+    },
   };
 }
 
 /** @ngInject */
 function compile($compile) {
   return {
-    restrict: "A",
+    restrict: 'A',
     link: function(scope, element, attrs) {
       scope.$watch(
         function(scope) {
@@ -55,42 +55,42 @@ function compile($compile) {
           $compile(element.contents())(scope);
         }
       );
-    }
+    },
   };
 }
 
 function watchChange() {
   return {
-    scope: { onchange: "&watchChange" },
+    scope: { onchange: '&watchChange' },
     link: function(scope, element) {
-      element.on("input", function() {
+      element.on('input', function() {
         scope.$apply(function() {
           scope.onchange({ inputValue: element.val() });
         });
       });
-    }
+    },
   };
 }
 
 /** @ngInject */
 function editorOptBool($compile) {
   return {
-    restrict: "E",
+    restrict: 'E',
     link: function(scope, elem, attrs) {
-      var ngchange = attrs.change ? ' ng-change="' + attrs.change + '"' : "";
-      var tip = attrs.tip ? " <tip>" + attrs.tip + "</tip>" : "";
-      var showIf = attrs.showIf ? ' ng-show="' + attrs.showIf + '" ' : "";
+      var ngchange = attrs.change ? ' ng-change="' + attrs.change + '"' : '';
+      var tip = attrs.tip ? ' <tip>' + attrs.tip + '</tip>' : '';
+      var showIf = attrs.showIf ? ' ng-show="' + attrs.showIf + '" ' : '';
 
       var template =
         '<div class="editor-option gf-form-checkbox text-center"' +
         showIf +
-        ">" +
+        '>' +
         ' <label for="' +
         attrs.model +
         '" class="small">' +
         attrs.text +
         tip +
-        "</label>" +
+        '</label>' +
         '<input class="cr1" id="' +
         attrs.model +
         '" type="checkbox" ' +
@@ -105,19 +105,19 @@ function editorOptBool($compile) {
         attrs.model +
         '" class="cr1"></label>';
       elem.replaceWith($compile(angular.element(template))(scope));
-    }
+    },
   };
 }
 
 /** @ngInject */
 function editorCheckbox($compile, $interpolate) {
   return {
-    restrict: "E",
+    restrict: 'E',
     link: function(scope, elem, attrs) {
       var text = $interpolate(attrs.text)(scope);
       var model = $interpolate(attrs.model)(scope);
-      var ngchange = attrs.change ? ' ng-change="' + attrs.change + '"' : "";
-      var tip = attrs.tip ? " <tip>" + attrs.tip + "</tip>" : "";
+      var ngchange = attrs.change ? ' ng-change="' + attrs.change + '"' : '';
+      var tip = attrs.tip ? ' <tip>' + attrs.tip + '</tip>' : '';
       var label =
         '<label for="' +
         scope.$id +
@@ -125,7 +125,7 @@ function editorCheckbox($compile, $interpolate) {
         '" class="checkbox-label">' +
         text +
         tip +
-        "</label>";
+        '</label>';
 
       var template =
         '<input class="cr1" id="' +
@@ -145,21 +145,21 @@ function editorCheckbox($compile, $interpolate) {
         '" class="cr1"></label>';
 
       template = template + label;
-      elem.addClass("gf-form-checkbox");
+      elem.addClass('gf-form-checkbox');
       elem.html($compile(angular.element(template))(scope));
-    }
+    },
   };
 }
 
 /** @ngInject */
 function gfDropdown($parse, $compile, $timeout) {
   function buildTemplate(items, placement?) {
-    var upclass = placement === "top" ? "dropup" : "";
+    var upclass = placement === 'top' ? 'dropup' : '';
     var ul = [
       '<ul class="dropdown-menu ' +
         upclass +
         '" role="menu" aria-labelledby="drop1">',
-      "</ul>"
+      '</ul>',
     ];
 
     for (let index = 0; index < items.length; index++) {
@@ -171,26 +171,26 @@ function gfDropdown($parse, $compile, $timeout) {
       }
 
       var li =
-        "<li" +
+        '<li' +
         (item.submenu && item.submenu.length
           ? ' class="dropdown-submenu"'
-          : "") +
-        ">" +
+          : '') +
+        '>' +
         '<a tabindex="-1" ng-href="' +
-        (item.href || "") +
+        (item.href || '') +
         '"' +
-        (item.click ? ' ng-click="' + item.click + '"' : "") +
-        (item.target ? ' target="' + item.target + '"' : "") +
-        (item.method ? ' data-method="' + item.method + '"' : "") +
-        ">" +
-        (item.text || "") +
-        "</a>";
+        (item.click ? ' ng-click="' + item.click + '"' : '') +
+        (item.target ? ' target="' + item.target + '"' : '') +
+        (item.method ? ' data-method="' + item.method + '"' : '') +
+        '>' +
+        (item.text || '') +
+        '</a>';
 
       if (item.submenu && item.submenu.length) {
-        li += buildTemplate(item.submenu).join("\n");
+        li += buildTemplate(item.submenu).join('\n');
       }
 
-      li += "</li>";
+      li += '</li>';
       ul.splice(index + 1, 0, li);
     }
 
@@ -198,29 +198,29 @@ function gfDropdown($parse, $compile, $timeout) {
   }
 
   return {
-    restrict: "EA",
+    restrict: 'EA',
     scope: true,
     link: function postLink(scope, iElement, iAttrs) {
       var getter = $parse(iAttrs.gfDropdown),
         items = getter(scope);
       $timeout(function() {
-        var placement = iElement.data("placement");
+        var placement = iElement.data('placement');
         var dropdown = angular.element(
-          buildTemplate(items, placement).join("")
+          buildTemplate(items, placement).join('')
         );
         dropdown.insertAfter(iElement);
-        $compile(iElement.next("ul.dropdown-menu"))(scope);
+        $compile(iElement.next('ul.dropdown-menu'))(scope);
       });
 
-      iElement.addClass("dropdown-toggle").attr("data-toggle", "dropdown");
-    }
+      iElement.addClass('dropdown-toggle').attr('data-toggle', 'dropdown');
+    },
   };
 }
 
-coreModule.directive("tip", tip);
-coreModule.directive("clipboardButton", clipboardButton);
-coreModule.directive("compile", compile);
-coreModule.directive("watchChange", watchChange);
-coreModule.directive("editorOptBool", editorOptBool);
-coreModule.directive("editorCheckbox", editorCheckbox);
-coreModule.directive("gfDropdown", gfDropdown);
+coreModule.directive('tip', tip);
+coreModule.directive('clipboardButton', clipboardButton);
+coreModule.directive('compile', compile);
+coreModule.directive('watchChange', watchChange);
+coreModule.directive('editorOptBool', editorOptBool);
+coreModule.directive('editorCheckbox', editorCheckbox);
+coreModule.directive('gfDropdown', gfDropdown);

+ 8 - 6
public/app/core/directives/ng_model_on_blur.ts

@@ -17,7 +17,7 @@ function ngModelOnBlur() {
           ngModelCtrl.$setViewValue(elm.val());
         });
       });
-    }
+    },
   };
 }
 
@@ -25,12 +25,14 @@ function emptyToNull() {
   return {
     restrict: 'A',
     require: 'ngModel',
-    link: function (scope, elm, attrs, ctrl) {
-      ctrl.$parsers.push(function (viewValue) {
-        if (viewValue === "") { return null; }
+    link: function(scope, elm, attrs, ctrl) {
+      ctrl.$parsers.push(function(viewValue) {
+        if (viewValue === '') {
+          return null;
+        }
         return viewValue;
       });
-    }
+    },
   };
 }
 
@@ -48,7 +50,7 @@ function validTimeSpan() {
         var info = rangeUtil.describeTextRange(viewValue);
         return info.invalid !== true;
       };
-    }
+    },
   };
 }
 

+ 9 - 5
public/app/core/directives/rebuild_on_change.ts

@@ -20,7 +20,6 @@ function getBlockNodes(nodes) {
 
 /** @ngInject **/
 function rebuildOnChange($animate) {
-
   return {
     multiElement: true,
     terminal: true,
@@ -48,7 +47,10 @@ function rebuildOnChange($animate) {
         }
       }
 
-      scope.$watch(attrs.property, function rebuildOnChangeAction(value, oldValue) {
+      scope.$watch(attrs.property, function rebuildOnChangeAction(
+        value,
+        oldValue
+      ) {
         if (childScope && value !== oldValue) {
           cleanUp();
         }
@@ -56,15 +58,17 @@ function rebuildOnChange($animate) {
         if (!childScope && (value || attrs.showNull)) {
           transclude(function(clone, newScope) {
             childScope = newScope;
-            clone[clone.length++] = document.createComment(' end rebuild on change ');
-            block = {clone: clone};
+            clone[clone.length++] = document.createComment(
+              ' end rebuild on change '
+            );
+            block = { clone: clone };
             $animate.enter(clone, elem.parent(), elem);
           });
         } else {
           cleanUp();
         }
       });
-    }
+    },
   };
 }
 

+ 94 - 36
public/app/core/directives/tags.ts

@@ -6,7 +6,7 @@ import 'vendor/tagsinput/bootstrap-tagsinput.js';
 function djb2(str) {
   var hash = 5381;
   for (var i = 0; i < str.length; i++) {
-    hash = ((hash << 5) + hash) + str.charCodeAt(i); /* hash * 33 + c */
+    hash = (hash << 5) + hash + str.charCodeAt(i); /* hash * 33 + c */
   }
   return hash;
 }
@@ -14,33 +14,79 @@ function djb2(str) {
 function setColor(name, element) {
   var hash = djb2(name.toLowerCase());
   var colors = [
-    "#E24D42","#1F78C1","#BA43A9","#705DA0","#466803",
-    "#508642","#447EBC","#C15C17","#890F02","#757575",
-    "#0A437C","#6D1F62","#584477","#629E51","#2F4F4F",
-    "#BF1B00","#806EB7","#8a2eb8", "#699e00","#000000",
-    "#3F6833","#2F575E","#99440A","#E0752D","#0E4AB4",
-    "#58140C","#052B51","#511749","#3F2B5B",
+    '#E24D42',
+    '#1F78C1',
+    '#BA43A9',
+    '#705DA0',
+    '#466803',
+    '#508642',
+    '#447EBC',
+    '#C15C17',
+    '#890F02',
+    '#757575',
+    '#0A437C',
+    '#6D1F62',
+    '#584477',
+    '#629E51',
+    '#2F4F4F',
+    '#BF1B00',
+    '#806EB7',
+    '#8a2eb8',
+    '#699e00',
+    '#000000',
+    '#3F6833',
+    '#2F575E',
+    '#99440A',
+    '#E0752D',
+    '#0E4AB4',
+    '#58140C',
+    '#052B51',
+    '#511749',
+    '#3F2B5B',
   ];
   var borderColors = [
-    "#FF7368","#459EE7","#E069CF","#9683C6","#6C8E29",
-    "#76AC68","#6AA4E2","#E7823D","#AF3528","#9B9B9B",
-    "#3069A2","#934588","#7E6A9D","#88C477","#557575",
-    "#E54126","#A694DD","#B054DE", "#8FC426","#262626",
-    "#658E59","#557D84","#BF6A30","#FF9B53","#3470DA",
-    "#7E3A32","#2B5177","#773D6F","#655181",
+    '#FF7368',
+    '#459EE7',
+    '#E069CF',
+    '#9683C6',
+    '#6C8E29',
+    '#76AC68',
+    '#6AA4E2',
+    '#E7823D',
+    '#AF3528',
+    '#9B9B9B',
+    '#3069A2',
+    '#934588',
+    '#7E6A9D',
+    '#88C477',
+    '#557575',
+    '#E54126',
+    '#A694DD',
+    '#B054DE',
+    '#8FC426',
+    '#262626',
+    '#658E59',
+    '#557D84',
+    '#BF6A30',
+    '#FF9B53',
+    '#3470DA',
+    '#7E3A32',
+    '#2B5177',
+    '#773D6F',
+    '#655181',
   ];
   var color = colors[Math.abs(hash % colors.length)];
   var borderColor = borderColors[Math.abs(hash % borderColors.length)];
-  element.css("background-color", color);
-  element.css("border-color", borderColor);
+  element.css('background-color', color);
+  element.css('border-color', borderColor);
 }
 
 function tagColorFromName() {
   return {
-    scope: { tagColorFromName: "=" },
-    link: function (scope, element) {
+    scope: { tagColorFromName: '=' },
+    link: function(scope, element) {
       setColor(scope.tagColorFromName, element);
-    }
+    },
   };
 }
 
@@ -63,12 +109,11 @@ function bootstrapTagsinput() {
     restrict: 'EA',
     scope: {
       model: '=ngModel',
-      onTagsUpdated: "&",
+      onTagsUpdated: '&',
     },
     template: '<select multiple></select>',
     replace: false,
     link: function(scope, element, attrs) {
-
       if (!angular.isArray(scope.model)) {
         scope.model = [];
       }
@@ -81,13 +126,18 @@ function bootstrapTagsinput() {
 
       select.tagsinput({
         typeahead: {
-          source: angular.isFunction(scope.$parent[attrs.typeaheadSource]) ? scope.$parent[attrs.typeaheadSource] : null
+          source: angular.isFunction(scope.$parent[attrs.typeaheadSource])
+            ? scope.$parent[attrs.typeaheadSource]
+            : null,
         },
         widthClass: attrs.widthClass,
         itemValue: getItemProperty(scope, attrs.itemvalue),
-        itemText : getItemProperty(scope, attrs.itemtext),
-        tagClass : angular.isFunction(scope.$parent[attrs.tagclass]) ?
-          scope.$parent[attrs.tagclass] : function() { return attrs.tagclass; }
+        itemText: getItemProperty(scope, attrs.itemtext),
+        tagClass: angular.isFunction(scope.$parent[attrs.tagclass])
+          ? scope.$parent[attrs.tagclass]
+          : function() {
+              return attrs.tagclass;
+            },
       });
 
       select.on('itemAdded', function(event) {
@@ -97,7 +147,12 @@ function bootstrapTagsinput() {
             scope.onTagsUpdated();
           }
         }
-        var tagElement = select.next().children("span").filter(function() { return $(this).text() === event.item; });
+        var tagElement = select
+          .next()
+          .children('span')
+          .filter(function() {
+            return $(this).text() === event.item;
+          });
         setColor(event.item, tagElement);
       });
 
@@ -111,19 +166,22 @@ function bootstrapTagsinput() {
         }
       });
 
-      scope.$watch("model", function() {
-        if (!angular.isArray(scope.model)) {
-          scope.model = [];
-        }
+      scope.$watch(
+        'model',
+        function() {
+          if (!angular.isArray(scope.model)) {
+            scope.model = [];
+          }
 
-        select.tagsinput('removeAll');
+          select.tagsinput('removeAll');
 
-        for (var i = 0; i < scope.model.length; i++) {
-          select.tagsinput('add', scope.model[i]);
-        }
-
-      }, true);
-    }
+          for (var i = 0; i < scope.model.length; i++) {
+            select.tagsinput('add', scope.model[i]);
+          }
+        },
+        true
+      );
+    },
   };
 }
 

+ 9 - 11
public/app/core/filters/filters.ts

@@ -41,19 +41,17 @@ coreModule.filter('moment', function() {
 
 coreModule.filter('noXml', function() {
   var noXml = function(text) {
-  return _.isString(text)
-    ? text
-    .replace(/&/g, '&amp;')
-    .replace(/</g, '&lt;')
-    .replace(/>/g, '&gt;')
-    .replace(/'/g, '&#39;')
-    .replace(/"/g, '&quot;')
-    : text;
+    return _.isString(text)
+      ? text
+          .replace(/&/g, '&amp;')
+          .replace(/</g, '&lt;')
+          .replace(/>/g, '&gt;')
+          .replace(/'/g, '&#39;')
+          .replace(/"/g, '&quot;')
+      : text;
   };
   return function(text) {
-    return _.isArray(text)
-      ? _.map(text, noXml)
-      : noXml(text);
+    return _.isArray(text) ? _.map(text, noXml) : noXml(text);
   };
 });
 

+ 20 - 16
public/app/core/live/live_srv.ts

@@ -1,7 +1,7 @@
 import _ from 'lodash';
 import config from 'app/core/config';
 
-import {Observable} from 'rxjs/Observable';
+import { Observable } from 'rxjs/Observable';
 
 export class LiveSrv {
   conn: any;
@@ -14,7 +14,12 @@ export class LiveSrv {
 
   getWebSocketUrl() {
     var l = window.location;
-    return ((l.protocol === "https:") ? "wss://" : "ws://") + l.host + config.appSubUrl + '/ws';
+    return (
+      (l.protocol === 'https:' ? 'wss://' : 'ws://') +
+      l.host +
+      config.appSubUrl +
+      '/ws'
+    );
   }
 
   getConnection() {
@@ -30,25 +35,25 @@ export class LiveSrv {
       console.log('Live: connecting...');
       this.conn = new WebSocket(this.getWebSocketUrl());
 
-      this.conn.onclose = (evt) => {
-        console.log("Live: websocket onclose", evt);
-        reject({message: 'Connection closed'});
+      this.conn.onclose = evt => {
+        console.log('Live: websocket onclose', evt);
+        reject({ message: 'Connection closed' });
 
         this.initPromise = null;
         setTimeout(this.reconnect.bind(this), 2000);
       };
 
-      this.conn.onmessage = (evt) => {
+      this.conn.onmessage = evt => {
         this.handleMessage(evt.data);
       };
 
-      this.conn.onerror = (evt) => {
+      this.conn.onerror = evt => {
         this.initPromise = null;
-        reject({message: 'Connection error'});
-        console.log("Live: websocket error", evt);
+        reject({ message: 'Connection error' });
+        console.log('Live: websocket error', evt);
       };
 
-      this.conn.onopen = (evt) => {
+      this.conn.onopen = evt => {
         console.log('opened');
         this.initPromise = null;
         resolve(this.conn);
@@ -62,7 +67,7 @@ export class LiveSrv {
     message = JSON.parse(message);
 
     if (!message.stream) {
-      console.log("Error: stream message without stream!", message);
+      console.log('Error: stream message without stream!', message);
       return;
     }
 
@@ -85,7 +90,7 @@ export class LiveSrv {
 
     this.getConnection().then(conn => {
       _.each(this.observers, (value, key) => {
-        this.send({action: 'subscribe', stream: key});
+        this.send({ action: 'subscribe', stream: key });
       });
     });
   }
@@ -98,7 +103,7 @@ export class LiveSrv {
     this.observers[stream] = observer;
 
     this.getConnection().then(conn => {
-      this.send({action: 'subscribe', stream: stream});
+      this.send({ action: 'subscribe', stream: stream });
     });
   }
 
@@ -107,7 +112,7 @@ export class LiveSrv {
     delete this.observers[stream];
 
     this.getConnection().then(conn => {
-      this.send({action: 'unsubscribe', stream: stream});
+      this.send({ action: 'unsubscribe', stream: stream });
     });
   }
 
@@ -126,8 +131,7 @@ export class LiveSrv {
     //   this.send({action: 'subscribe', stream: name});
     // });
   }
-
 }
 
 var instance = new LiveSrv();
-export {instance as liveSrv};
+export { instance as liveSrv };

+ 5 - 9
public/app/core/mod_defs.d.ts

@@ -1,18 +1,14 @@
-declare module "app/core/controllers/all" {
+declare module 'app/core/controllers/all' {
   let json: any;
-  export {json};
+  export { json };
 }
 
-declare module "app/core/routes/all" {
+declare module 'app/core/routes/all' {
   let json: any;
-  export {json};
+  export { json };
 }
 
-declare module "app/core/services/all" {
+declare module 'app/core/services/all' {
   let json: any;
   export default json;
 }
-
-
-
-

+ 6 - 6
public/app/core/nav_model_srv.ts

@@ -34,7 +34,7 @@ export class NavModelSrv {
   }
 
   getCfgNode() {
-    return _.find(this.navItems, {id: 'cfg'});
+    return _.find(this.navItems, { id: 'cfg' });
   }
 
   getNav(...args) {
@@ -48,7 +48,7 @@ export class NavModelSrv {
         break;
       }
 
-      let node = _.find(children, {id: id});
+      let node = _.find(children, { id: id });
       nav.breadcrumbs.push(node);
       nav.node = node;
       nav.main = node;
@@ -70,15 +70,15 @@ export class NavModelSrv {
 
   getNotFoundNav() {
     var node = {
-      text: "Page not found",
-      icon: "fa fa-fw fa-warning",
-      subTitle: "404 Error"
+      text: 'Page not found',
+      icon: 'fa fa-fw fa-warning',
+      subTitle: '404 Error',
     };
 
     return {
       breadcrumbs: [node],
       node: node,
-      main: node
+      main: node,
     };
   }
 }

+ 45 - 19
public/app/core/profiler.ts

@@ -20,15 +20,30 @@ export class Profiler {
       return;
     }
 
-    $rootScope.$watch(() => {
-      this.digestCounter++;
-      return false;
-    }, () => {});
+    $rootScope.$watch(
+      () => {
+        this.digestCounter++;
+        return false;
+      },
+      () => {}
+    );
 
     $rootScope.onAppEvent('refresh', this.refresh.bind(this), $rootScope);
-    $rootScope.onAppEvent('dashboard-fetch-end', this.dashboardFetched.bind(this), $rootScope);
-    $rootScope.onAppEvent('dashboard-initialized', this.dashboardInitialized.bind(this), $rootScope);
-    $rootScope.onAppEvent('panel-initialized', this.panelInitialized.bind(this), $rootScope);
+    $rootScope.onAppEvent(
+      'dashboard-fetch-end',
+      this.dashboardFetched.bind(this),
+      $rootScope
+    );
+    $rootScope.onAppEvent(
+      'dashboard-initialized',
+      this.dashboardInitialized.bind(this),
+      $rootScope
+    );
+    $rootScope.onAppEvent(
+      'panel-initialized',
+      this.panelInitialized.bind(this),
+      $rootScope
+    );
   }
 
   refresh() {
@@ -55,12 +70,21 @@ export class Profiler {
 
   dashboardInitialized() {
     setTimeout(() => {
-      console.log("Dashboard::Performance Total Digests: " + this.digestCounter);
-      console.log("Dashboard::Performance Total Watchers: " + this.getTotalWatcherCount());
-      console.log("Dashboard::Performance Total ScopeCount: " + this.scopeCount);
-
-      var timeTaken = this.timings.lastPanelInitializedAt - this.timings.dashboardLoadStart;
-      console.log("Dashboard::Performance All panels initialized in " + timeTaken + " ms");
+      console.log(
+        'Dashboard::Performance Total Digests: ' + this.digestCounter
+      );
+      console.log(
+        'Dashboard::Performance Total Watchers: ' + this.getTotalWatcherCount()
+      );
+      console.log(
+        'Dashboard::Performance Total ScopeCount: ' + this.scopeCount
+      );
+
+      var timeTaken =
+        this.timings.lastPanelInitializedAt - this.timings.dashboardLoadStart;
+      console.log(
+        'Dashboard::Performance All panels initialized in ' + timeTaken + ' ms'
+      );
 
       // measure digest performance
       var rootDigestStart = window.performance.now();
@@ -68,7 +92,10 @@ export class Profiler {
         this.$rootScope.$apply();
       }
 
-      console.log("Dashboard::Performance Root Digest " + ((window.performance.now() - rootDigestStart) / 30));
+      console.log(
+        'Dashboard::Performance Root Digest ' +
+          (window.performance.now() - rootDigestStart) / 30
+      );
     }, 3000);
   }
 
@@ -77,15 +104,15 @@ export class Profiler {
     var scopes = 0;
     var root = $(document.getElementsByTagName('body'));
 
-    var f = function (element) {
+    var f = function(element) {
       if (element.data().hasOwnProperty('$scope')) {
         scopes++;
-        angular.forEach(element.data().$scope.$$watchers, function () {
+        angular.forEach(element.data().$scope.$$watchers, function() {
           count++;
         });
       }
 
-      angular.forEach(element.children(), function (childElement) {
+      angular.forEach(element.children(), function(childElement) {
         f($(childElement));
       });
     };
@@ -116,8 +143,7 @@ export class Profiler {
     this.panelsInitCount++;
     this.timings.lastPanelInitializedAt = new Date().getTime();
   }
-
 }
 
 var profiler = new Profiler();
-export {profiler};
+export { profiler };

+ 15 - 11
public/app/core/routes/bundle_loader.ts

@@ -4,19 +4,23 @@ export class BundleLoader {
   constructor(bundleName) {
     var defer = null;
 
-    this.lazy = ["$q", "$route", "$rootScope", ($q, $route, $rootScope) => {
-      if (defer) {
-        return defer.promise;
-      }
-
-      defer = $q.defer();
+    this.lazy = [
+      '$q',
+      '$route',
+      '$rootScope',
+      ($q, $route, $rootScope) => {
+        if (defer) {
+          return defer.promise;
+        }
 
-      System.import(bundleName).then(() => {
-        defer.resolve();
-      });
+        defer = $q.defer();
 
-      return defer.promise;
-    }];
+        System.import(bundleName).then(() => {
+          defer.resolve();
+        });
 
+        return defer.promise;
+      },
+    ];
   }
 }

+ 25 - 22
public/app/core/routes/dashboard_loaders.ts

@@ -1,10 +1,9 @@
 import coreModule from '../core_module';
 
 export class LoadDashboardCtrl {
-
   /** @ngInject */
   constructor($scope, $routeParams, dashboardLoaderSrv, backendSrv, $location) {
-    $scope.appEvent("dashboard-fetch-start");
+    $scope.appEvent('dashboard-fetch-start');
 
     if (!$routeParams.slug) {
       backendSrv.get('/api/dashboards/home').then(function(homeDash) {
@@ -19,33 +18,37 @@ export class LoadDashboardCtrl {
       return;
     }
 
-    dashboardLoaderSrv.loadDashboard($routeParams.type, $routeParams.slug).then(function(result) {
-      if ($routeParams.keepRows) {
-        result.meta.keepRows = true;
-      }
-      $scope.initDashboard(result, $scope);
-    });
+    dashboardLoaderSrv
+      .loadDashboard($routeParams.type, $routeParams.slug)
+      .then(function(result) {
+        if ($routeParams.keepRows) {
+          result.meta.keepRows = true;
+        }
+        $scope.initDashboard(result, $scope);
+      });
   }
 }
 
 export class NewDashboardCtrl {
-
   /** @ngInject */
   constructor($scope, $routeParams) {
-    $scope.initDashboard({
-      meta: { canStar: false, canShare: false, isNew: true },
-      dashboard: {
-        title: "New dashboard",
-        panels: [
-          {
-            type: 'add-panel',
-            gridPos: {x: 0, y: 0, w: 12, h: 9},
-            title: 'Panel Title',
-          }
-        ],
-        folderId: Number($routeParams.folderId)
+    $scope.initDashboard(
+      {
+        meta: { canStar: false, canShare: false, isNew: true },
+        dashboard: {
+          title: 'New dashboard',
+          panels: [
+            {
+              type: 'add-panel',
+              gridPos: { x: 0, y: 0, w: 12, h: 9 },
+              title: 'Panel Title',
+            },
+          ],
+          folderId: Number($routeParams.folderId),
+        },
       },
-    }, $scope);
+      $scope
+    );
   }
 }
 

+ 284 - 262
public/app/core/routes/routes.ts

@@ -6,277 +6,299 @@ function setupAngularRoutes($routeProvider, $locationProvider) {
   $locationProvider.html5Mode(true);
 
   var loadOrgBundle = {
-    lazy: ["$q", "$route", "$rootScope", ($q, $route, $rootScope) => {
-      return System.import('app/features/org/all');
-    }]
+    lazy: [
+      '$q',
+      '$route',
+      '$rootScope',
+      ($q, $route, $rootScope) => {
+        return System.import('app/features/org/all');
+      },
+    ],
   };
 
   var loadAdminBundle = {
-    lazy: ["$q", "$route", "$rootScope", ($q, $route, $rootScope) => {
-      return System.import('app/features/admin/admin');
-    }]
+    lazy: [
+      '$q',
+      '$route',
+      '$rootScope',
+      ($q, $route, $rootScope) => {
+        return System.import('app/features/admin/admin');
+      },
+    ],
   };
 
   var loadAlertingBundle = {
-    lazy: ["$q", "$route", "$rootScope", ($q, $route, $rootScope) => {
-      return System.import('app/features/alerting/all');
-    }]
+    lazy: [
+      '$q',
+      '$route',
+      '$rootScope',
+      ($q, $route, $rootScope) => {
+        return System.import('app/features/alerting/all');
+      },
+    ],
   };
 
   $routeProvider
-  .when('/', {
-    templateUrl: 'public/app/partials/dashboard.html',
-    controller : 'LoadDashboardCtrl',
-    reloadOnSearch: false,
-    pageClass: 'page-dashboard',
-  })
-  .when('/dashboard/:type/:slug', {
-    templateUrl: 'public/app/partials/dashboard.html',
-    controller : 'LoadDashboardCtrl',
-    reloadOnSearch: false,
-    pageClass: 'page-dashboard',
-  })
-  .when('/dashboard-solo/:type/:slug', {
-    templateUrl: 'public/app/features/panel/partials/soloPanel.html',
-    controller : 'SoloPanelCtrl',
-    reloadOnSearch: false,
-    pageClass: 'page-dashboard',
-  })
-  .when('/dashboard/new', {
-    templateUrl: 'public/app/partials/dashboard.html',
-    controller : 'NewDashboardCtrl',
-    reloadOnSearch: false,
-    pageClass: 'page-dashboard',
-  })
-  .when('/dashboard/import', {
-    templateUrl: 'public/app/features/dashboard/partials/dashboardImport.html',
-    controller : 'DashboardImportCtrl',
-    controllerAs: 'ctrl',
-  })
-  .when('/datasources', {
-    templateUrl: 'public/app/features/plugins/partials/ds_list.html',
-    controller : 'DataSourcesCtrl',
-    controllerAs: 'ctrl',
-  })
-  .when('/datasources/edit/:id', {
-    templateUrl: 'public/app/features/plugins/partials/ds_edit.html',
-    controller : 'DataSourceEditCtrl',
-    controllerAs: 'ctrl',
-  })
-  .when('/datasources/new', {
-    templateUrl: 'public/app/features/plugins/partials/ds_edit.html',
-    controller : 'DataSourceEditCtrl',
-    controllerAs: 'ctrl',
-  })
-  .when('/dashboards', {
-    templateUrl: 'public/app/features/dashboard/partials/dashboard_list.html',
-    controller : 'DashboardListCtrl',
-    controllerAs: 'ctrl',
-  })
-  .when('/dashboards/folder/new', {
-    templateUrl: 'public/app/features/dashboard/partials/create_folder.html',
-    controller : 'CreateFolderCtrl',
-    controllerAs: 'ctrl',
-  })
-  .when('/dashboards/folder/:folderId/:slug/permissions', {
-    templateUrl: 'public/app/features/dashboard/partials/folder_permissions.html',
-    controller : 'FolderPermissionsCtrl',
-    controllerAs: 'ctrl',
-  })
-  .when('/dashboards/folder/:folderId/:slug/settings', {
-    templateUrl: 'public/app/features/dashboard/partials/folder_settings.html',
-    controller : 'FolderSettingsCtrl',
-    controllerAs: 'ctrl',
-  })
-  .when('/dashboards/folder/:folderId/:slug', {
-    templateUrl: 'public/app/features/dashboard/partials/folder_dashboards.html',
-    controller : 'FolderDashboardsCtrl',
-    controllerAs: 'ctrl',
-  })
-  .when('/org', {
-    templateUrl: 'public/app/features/org/partials/orgDetails.html',
-    controller : 'OrgDetailsCtrl',
-    resolve: loadOrgBundle,
-  })
-  .when('/org/new', {
-    templateUrl: 'public/app/features/org/partials/newOrg.html',
-    controller : 'NewOrgCtrl',
-    resolve: loadOrgBundle,
-  })
-  .when('/org/users', {
-    templateUrl: 'public/app/features/org/partials/orgUsers.html',
-    controller : 'OrgUsersCtrl',
-    controllerAs: 'ctrl',
-    resolve: loadOrgBundle,
-  })
-  .when('/org/users/invite', {
-    templateUrl: 'public/app/features/org/partials/invite.html',
-    controller : 'UserInviteCtrl',
-    controllerAs: 'ctrl',
-    resolve: loadOrgBundle,
-  })
-  .when('/org/apikeys', {
-    templateUrl: 'public/app/features/org/partials/orgApiKeys.html',
-    controller : 'OrgApiKeysCtrl',
-    resolve: loadOrgBundle,
-  })
-  .when('/org/teams', {
-    templateUrl: 'public/app/features/org/partials/teams.html',
-    controller : 'TeamsCtrl',
-    controllerAs: 'ctrl',
-    resolve: loadOrgBundle,
-  })
-  .when('/org/teams/edit/:id', {
-    templateUrl: 'public/app/features/org/partials/team_details.html',
-    controller : 'TeamDetailsCtrl',
-    controllerAs: 'ctrl',
-    resolve: loadOrgBundle,
-  })
-  .when('/profile', {
-    templateUrl: 'public/app/features/org/partials/profile.html',
-    controller : 'ProfileCtrl',
-    controllerAs: 'ctrl',
-    resolve: loadOrgBundle,
-  })
-  .when('/profile/password', {
-    templateUrl: 'public/app/features/org/partials/change_password.html',
-    controller : 'ChangePasswordCtrl',
-    resolve: loadOrgBundle,
-  })
-  .when('/profile/select-org', {
-    templateUrl: 'public/app/features/org/partials/select_org.html',
-    controller : 'SelectOrgCtrl',
-    resolve: loadOrgBundle,
-  })
-  // ADMIN
-  .when('/admin', {
-    templateUrl: 'public/app/features/admin/partials/admin_home.html',
-    controller : 'AdminHomeCtrl',
-    controllerAs: 'ctrl',
-    resolve: loadAdminBundle,
-  })
-  .when('/admin/settings', {
-    templateUrl: 'public/app/features/admin/partials/settings.html',
-    controller : 'AdminSettingsCtrl',
-    controllerAs: 'ctrl',
-    resolve: loadAdminBundle,
-  })
-  .when('/admin/users', {
-    templateUrl: 'public/app/features/admin/partials/users.html',
-    controller : 'AdminListUsersCtrl',
-    controllerAs: 'ctrl',
-    resolve: loadAdminBundle,
-  })
-  .when('/admin/users/create', {
-    templateUrl: 'public/app/features/admin/partials/new_user.html',
-    controller : 'AdminEditUserCtrl',
-    resolve: loadAdminBundle,
-  })
-  .when('/admin/users/edit/:id', {
-    templateUrl: 'public/app/features/admin/partials/edit_user.html',
-    controller : 'AdminEditUserCtrl',
-    resolve: loadAdminBundle,
-  })
-  .when('/admin/orgs', {
-    templateUrl: 'public/app/features/admin/partials/orgs.html',
-    controller : 'AdminListOrgsCtrl',
-    controllerAs: 'ctrl',
-    resolve: loadAdminBundle,
-  })
-  .when('/admin/orgs/edit/:id', {
-    templateUrl: 'public/app/features/admin/partials/edit_org.html',
-    controller : 'AdminEditOrgCtrl',
-    controllerAs: 'ctrl',
-    resolve: loadAdminBundle,
-  })
-  .when('/admin/stats', {
-    templateUrl: 'public/app/features/admin/partials/stats.html',
-    controller : 'AdminStatsCtrl',
-    controllerAs: 'ctrl',
-    resolve: loadAdminBundle,
-  })
-  // LOGIN / SIGNUP
-  .when('/login', {
-    templateUrl: 'public/app/partials/login.html',
-    controller : 'LoginCtrl',
-    pageClass: 'login-page sidemenu-hidden',
-  })
-  .when('/invite/:code', {
-    templateUrl: 'public/app/partials/signup_invited.html',
-    controller : 'InvitedCtrl',
-    pageClass: 'sidemenu-hidden',
-  })
-  .when('/signup', {
-    templateUrl: 'public/app/partials/signup_step2.html',
-    controller : 'SignUpCtrl',
-    pageClass: 'sidemenu-hidden',
-  })
-  .when('/user/password/send-reset-email', {
-    templateUrl: 'public/app/partials/reset_password.html',
-    controller : 'ResetPasswordCtrl',
-    pageClass: 'sidemenu-hidden',
-  })
-  .when('/user/password/reset', {
-    templateUrl: 'public/app/partials/reset_password.html',
-    controller : 'ResetPasswordCtrl',
-    pageClass: 'sidemenu-hidden',
-  })
-  .when('/dashboard/snapshots', {
-    templateUrl: 'public/app/features/snapshot/partials/snapshots.html',
-    controller : 'SnapshotsCtrl',
-    controllerAs: 'ctrl',
-  })
-  .when('/plugins', {
-    templateUrl: 'public/app/features/plugins/partials/plugin_list.html',
-    controller: 'PluginListCtrl',
-    controllerAs: 'ctrl',
-  })
-  .when('/plugins/:pluginId/edit', {
-    templateUrl: 'public/app/features/plugins/partials/plugin_edit.html',
-    controller: 'PluginEditCtrl',
-    controllerAs: 'ctrl',
-  })
-  .when('/plugins/:pluginId/page/:slug', {
-    templateUrl: 'public/app/features/plugins/partials/plugin_page.html',
-    controller: 'AppPageCtrl',
-    controllerAs: 'ctrl',
-  })
-  .when('/styleguide/:page?', {
-    controller: 'StyleGuideCtrl',
-    controllerAs: 'ctrl',
-    templateUrl: 'public/app/features/styleguide/styleguide.html',
-  })
-  .when('/alerting', {
-    redirectTo: '/alerting/list'
-  })
-  .when('/alerting/list', {
-    templateUrl: 'public/app/features/alerting/partials/alert_list.html',
-    controller: 'AlertListCtrl',
-    controllerAs: 'ctrl',
-    resolve: loadAlertingBundle,
-  })
-  .when('/alerting/notifications', {
-    templateUrl: 'public/app/features/alerting/partials/notifications_list.html',
-    controller: 'AlertNotificationsListCtrl',
-    controllerAs: 'ctrl',
-    resolve: loadAlertingBundle,
-  })
-  .when('/alerting/notification/new', {
-    templateUrl: 'public/app/features/alerting/partials/notification_edit.html',
-    controller: 'AlertNotificationEditCtrl',
-    controllerAs: 'ctrl',
-    resolve: loadAlertingBundle,
-  })
-  .when('/alerting/notification/:id/edit', {
-    templateUrl: 'public/app/features/alerting/partials/notification_edit.html',
-    controller: 'AlertNotificationEditCtrl',
-    controllerAs: 'ctrl',
-    resolve: loadAlertingBundle,
-  })
-  .otherwise({
-    templateUrl: 'public/app/partials/error.html',
-    controller: 'ErrorCtrl'
-  });
+    .when('/', {
+      templateUrl: 'public/app/partials/dashboard.html',
+      controller: 'LoadDashboardCtrl',
+      reloadOnSearch: false,
+      pageClass: 'page-dashboard',
+    })
+    .when('/dashboard/:type/:slug', {
+      templateUrl: 'public/app/partials/dashboard.html',
+      controller: 'LoadDashboardCtrl',
+      reloadOnSearch: false,
+      pageClass: 'page-dashboard',
+    })
+    .when('/dashboard-solo/:type/:slug', {
+      templateUrl: 'public/app/features/panel/partials/soloPanel.html',
+      controller: 'SoloPanelCtrl',
+      reloadOnSearch: false,
+      pageClass: 'page-dashboard',
+    })
+    .when('/dashboard/new', {
+      templateUrl: 'public/app/partials/dashboard.html',
+      controller: 'NewDashboardCtrl',
+      reloadOnSearch: false,
+      pageClass: 'page-dashboard',
+    })
+    .when('/dashboard/import', {
+      templateUrl:
+        'public/app/features/dashboard/partials/dashboard_import.html',
+      controller: 'DashboardImportCtrl',
+      controllerAs: 'ctrl',
+    })
+    .when('/datasources', {
+      templateUrl: 'public/app/features/plugins/partials/ds_list.html',
+      controller: 'DataSourcesCtrl',
+      controllerAs: 'ctrl',
+    })
+    .when('/datasources/edit/:id', {
+      templateUrl: 'public/app/features/plugins/partials/ds_edit.html',
+      controller: 'DataSourceEditCtrl',
+      controllerAs: 'ctrl',
+    })
+    .when('/datasources/new', {
+      templateUrl: 'public/app/features/plugins/partials/ds_edit.html',
+      controller: 'DataSourceEditCtrl',
+      controllerAs: 'ctrl',
+    })
+    .when('/dashboards', {
+      templateUrl: 'public/app/features/dashboard/partials/dashboard_list.html',
+      controller: 'DashboardListCtrl',
+      controllerAs: 'ctrl',
+    })
+    .when('/dashboards/folder/new', {
+      templateUrl: 'public/app/features/dashboard/partials/create_folder.html',
+      controller: 'CreateFolderCtrl',
+      controllerAs: 'ctrl',
+    })
+    .when('/dashboards/folder/:folderId/:slug/permissions', {
+      templateUrl:
+        'public/app/features/dashboard/partials/folder_permissions.html',
+      controller: 'FolderPermissionsCtrl',
+      controllerAs: 'ctrl',
+    })
+    .when('/dashboards/folder/:folderId/:slug/settings', {
+      templateUrl:
+        'public/app/features/dashboard/partials/folder_settings.html',
+      controller: 'FolderSettingsCtrl',
+      controllerAs: 'ctrl',
+    })
+    .when('/dashboards/folder/:folderId/:slug', {
+      templateUrl:
+        'public/app/features/dashboard/partials/folder_dashboards.html',
+      controller: 'FolderDashboardsCtrl',
+      controllerAs: 'ctrl',
+    })
+    .when('/org', {
+      templateUrl: 'public/app/features/org/partials/orgDetails.html',
+      controller: 'OrgDetailsCtrl',
+      resolve: loadOrgBundle,
+    })
+    .when('/org/new', {
+      templateUrl: 'public/app/features/org/partials/newOrg.html',
+      controller: 'NewOrgCtrl',
+      resolve: loadOrgBundle,
+    })
+    .when('/org/users', {
+      templateUrl: 'public/app/features/org/partials/orgUsers.html',
+      controller: 'OrgUsersCtrl',
+      controllerAs: 'ctrl',
+      resolve: loadOrgBundle,
+    })
+    .when('/org/users/invite', {
+      templateUrl: 'public/app/features/org/partials/invite.html',
+      controller: 'UserInviteCtrl',
+      controllerAs: 'ctrl',
+      resolve: loadOrgBundle,
+    })
+    .when('/org/apikeys', {
+      templateUrl: 'public/app/features/org/partials/orgApiKeys.html',
+      controller: 'OrgApiKeysCtrl',
+      resolve: loadOrgBundle,
+    })
+    .when('/org/teams', {
+      templateUrl: 'public/app/features/org/partials/teams.html',
+      controller: 'TeamsCtrl',
+      controllerAs: 'ctrl',
+      resolve: loadOrgBundle,
+    })
+    .when('/org/teams/edit/:id', {
+      templateUrl: 'public/app/features/org/partials/team_details.html',
+      controller: 'TeamDetailsCtrl',
+      controllerAs: 'ctrl',
+      resolve: loadOrgBundle,
+    })
+    .when('/profile', {
+      templateUrl: 'public/app/features/org/partials/profile.html',
+      controller: 'ProfileCtrl',
+      controllerAs: 'ctrl',
+      resolve: loadOrgBundle,
+    })
+    .when('/profile/password', {
+      templateUrl: 'public/app/features/org/partials/change_password.html',
+      controller: 'ChangePasswordCtrl',
+      resolve: loadOrgBundle,
+    })
+    .when('/profile/select-org', {
+      templateUrl: 'public/app/features/org/partials/select_org.html',
+      controller: 'SelectOrgCtrl',
+      resolve: loadOrgBundle,
+    })
+    // ADMIN
+    .when('/admin', {
+      templateUrl: 'public/app/features/admin/partials/admin_home.html',
+      controller: 'AdminHomeCtrl',
+      controllerAs: 'ctrl',
+      resolve: loadAdminBundle,
+    })
+    .when('/admin/settings', {
+      templateUrl: 'public/app/features/admin/partials/settings.html',
+      controller: 'AdminSettingsCtrl',
+      controllerAs: 'ctrl',
+      resolve: loadAdminBundle,
+    })
+    .when('/admin/users', {
+      templateUrl: 'public/app/features/admin/partials/users.html',
+      controller: 'AdminListUsersCtrl',
+      controllerAs: 'ctrl',
+      resolve: loadAdminBundle,
+    })
+    .when('/admin/users/create', {
+      templateUrl: 'public/app/features/admin/partials/new_user.html',
+      controller: 'AdminEditUserCtrl',
+      resolve: loadAdminBundle,
+    })
+    .when('/admin/users/edit/:id', {
+      templateUrl: 'public/app/features/admin/partials/edit_user.html',
+      controller: 'AdminEditUserCtrl',
+      resolve: loadAdminBundle,
+    })
+    .when('/admin/orgs', {
+      templateUrl: 'public/app/features/admin/partials/orgs.html',
+      controller: 'AdminListOrgsCtrl',
+      controllerAs: 'ctrl',
+      resolve: loadAdminBundle,
+    })
+    .when('/admin/orgs/edit/:id', {
+      templateUrl: 'public/app/features/admin/partials/edit_org.html',
+      controller: 'AdminEditOrgCtrl',
+      controllerAs: 'ctrl',
+      resolve: loadAdminBundle,
+    })
+    .when('/admin/stats', {
+      templateUrl: 'public/app/features/admin/partials/stats.html',
+      controller: 'AdminStatsCtrl',
+      controllerAs: 'ctrl',
+      resolve: loadAdminBundle,
+    })
+    // LOGIN / SIGNUP
+    .when('/login', {
+      templateUrl: 'public/app/partials/login.html',
+      controller: 'LoginCtrl',
+      pageClass: 'login-page sidemenu-hidden',
+    })
+    .when('/invite/:code', {
+      templateUrl: 'public/app/partials/signup_invited.html',
+      controller: 'InvitedCtrl',
+      pageClass: 'sidemenu-hidden',
+    })
+    .when('/signup', {
+      templateUrl: 'public/app/partials/signup_step2.html',
+      controller: 'SignUpCtrl',
+      pageClass: 'sidemenu-hidden',
+    })
+    .when('/user/password/send-reset-email', {
+      templateUrl: 'public/app/partials/reset_password.html',
+      controller: 'ResetPasswordCtrl',
+      pageClass: 'sidemenu-hidden',
+    })
+    .when('/user/password/reset', {
+      templateUrl: 'public/app/partials/reset_password.html',
+      controller: 'ResetPasswordCtrl',
+      pageClass: 'sidemenu-hidden',
+    })
+    .when('/dashboard/snapshots', {
+      templateUrl: 'public/app/features/snapshot/partials/snapshots.html',
+      controller: 'SnapshotsCtrl',
+      controllerAs: 'ctrl',
+    })
+    .when('/plugins', {
+      templateUrl: 'public/app/features/plugins/partials/plugin_list.html',
+      controller: 'PluginListCtrl',
+      controllerAs: 'ctrl',
+    })
+    .when('/plugins/:pluginId/edit', {
+      templateUrl: 'public/app/features/plugins/partials/plugin_edit.html',
+      controller: 'PluginEditCtrl',
+      controllerAs: 'ctrl',
+    })
+    .when('/plugins/:pluginId/page/:slug', {
+      templateUrl: 'public/app/features/plugins/partials/plugin_page.html',
+      controller: 'AppPageCtrl',
+      controllerAs: 'ctrl',
+    })
+    .when('/styleguide/:page?', {
+      controller: 'StyleGuideCtrl',
+      controllerAs: 'ctrl',
+      templateUrl: 'public/app/features/styleguide/styleguide.html',
+    })
+    .when('/alerting', {
+      redirectTo: '/alerting/list',
+    })
+    .when('/alerting/list', {
+      templateUrl: 'public/app/features/alerting/partials/alert_list.html',
+      controller: 'AlertListCtrl',
+      controllerAs: 'ctrl',
+      resolve: loadAlertingBundle,
+    })
+    .when('/alerting/notifications', {
+      templateUrl:
+        'public/app/features/alerting/partials/notifications_list.html',
+      controller: 'AlertNotificationsListCtrl',
+      controllerAs: 'ctrl',
+      resolve: loadAlertingBundle,
+    })
+    .when('/alerting/notification/new', {
+      templateUrl:
+        'public/app/features/alerting/partials/notification_edit.html',
+      controller: 'AlertNotificationEditCtrl',
+      controllerAs: 'ctrl',
+      resolve: loadAlertingBundle,
+    })
+    .when('/alerting/notification/:id/edit', {
+      templateUrl:
+        'public/app/features/alerting/partials/notification_edit.html',
+      controller: 'AlertNotificationEditCtrl',
+      controllerAs: 'ctrl',
+      resolve: loadAlertingBundle,
+    })
+    .otherwise({
+      templateUrl: 'public/app/partials/error.html',
+      controller: 'ErrorCtrl',
+    });
 }
 
 coreModule.config(setupAngularRoutes);

+ 47 - 25
public/app/core/services/alert_srv.ts

@@ -14,29 +14,50 @@ export class AlertSrv {
   }
 
   init() {
-    this.$rootScope.onAppEvent('alert-error', (e, alert) => {
-      this.set(alert[0], alert[1], 'error', 12000);
-    }, this.$rootScope);
-
-    this.$rootScope.onAppEvent('alert-warning', (e, alert) => {
-      this.set(alert[0], alert[1], 'warning', 5000);
-    }, this.$rootScope);
-
-    this.$rootScope.onAppEvent('alert-success', (e, alert) => {
-      this.set(alert[0], alert[1], 'success', 3000);
-    }, this.$rootScope);
-
-    appEvents.on('alert-warning', options => this.set(options[0], options[1], 'warning', 5000));
-    appEvents.on('alert-success', options => this.set(options[0], options[1], 'success', 3000));
-    appEvents.on('alert-error', options => this.set(options[0], options[1], 'error', 7000));
+    this.$rootScope.onAppEvent(
+      'alert-error',
+      (e, alert) => {
+        this.set(alert[0], alert[1], 'error', 12000);
+      },
+      this.$rootScope
+    );
+
+    this.$rootScope.onAppEvent(
+      'alert-warning',
+      (e, alert) => {
+        this.set(alert[0], alert[1], 'warning', 5000);
+      },
+      this.$rootScope
+    );
+
+    this.$rootScope.onAppEvent(
+      'alert-success',
+      (e, alert) => {
+        this.set(alert[0], alert[1], 'success', 3000);
+      },
+      this.$rootScope
+    );
+
+    appEvents.on('alert-warning', options =>
+      this.set(options[0], options[1], 'warning', 5000)
+    );
+    appEvents.on('alert-success', options =>
+      this.set(options[0], options[1], 'success', 3000)
+    );
+    appEvents.on('alert-error', options =>
+      this.set(options[0], options[1], 'error', 7000)
+    );
     appEvents.on('confirm-modal', this.showConfirmModal.bind(this));
   }
 
   getIconForSeverity(severity) {
     switch (severity) {
-      case 'success': return 'fa fa-check';
-      case 'error': return 'fa fa-exclamation-triangle';
-      default: return 'fa fa-exclamation';
+      case 'success':
+        return 'fa fa-check';
+      case 'error':
+        return 'fa fa-exclamation-triangle';
+      default:
+        return 'fa fa-exclamation';
     }
   }
 
@@ -52,7 +73,7 @@ export class AlertSrv {
       title: title || '',
       text: text || '',
       severity: severity || 'info',
-      icon: this.getIconForSeverity(severity)
+      icon: this.getIconForSeverity(severity),
     };
 
     var newAlertJson = angular.toJson(newAlert);
@@ -73,7 +94,7 @@ export class AlertSrv {
       this.$rootScope.$digest();
     }
 
-    return(newAlert);
+    return newAlert;
   }
 
   clear(alert) {
@@ -93,7 +114,8 @@ export class AlertSrv {
     };
 
     scope.updateConfirmText = function(value) {
-      scope.confirmTextValid = payload.confirmText.toLowerCase() === value.toLowerCase();
+      scope.confirmTextValid =
+        payload.confirmText.toLowerCase() === value.toLowerCase();
     };
 
     scope.title = payload.title;
@@ -104,9 +126,9 @@ export class AlertSrv {
     scope.onConfirm = payload.onConfirm;
     scope.onAltAction = payload.onAltAction;
     scope.altActionText = payload.altActionText;
-    scope.icon = payload.icon || "fa-check";
-    scope.yesText = payload.yesText || "Yes";
-    scope.noText = payload.noText || "Cancel";
+    scope.icon = payload.icon || 'fa-check';
+    scope.yesText = payload.yesText || 'Yes';
+    scope.noText = payload.noText || 'Cancel';
     scope.confirmTextValid = scope.confirmText ? false : true;
 
     var confirmModal = this.$modal({
@@ -115,7 +137,7 @@ export class AlertSrv {
       modalClass: 'confirm-modal',
       show: false,
       scope: scope,
-      keyboard: false
+      keyboard: false,
     });
 
     confirmModal.then(function(modalEl) {

+ 7 - 5
public/app/core/services/analytics.ts

@@ -3,14 +3,17 @@ import coreModule from 'app/core/core_module';
 import config from 'app/core/config';
 
 export class Analytics {
-
   /** @ngInject */
-  constructor(private $rootScope, private $location) {
-  }
+  constructor(private $rootScope, private $location) {}
 
   gaInit() {
     $.getScript('https://www.google-analytics.com/analytics.js'); // jQuery shortcut
-    var ga = (<any>window).ga = (<any>window).ga || function () { (ga.q = ga.q || []).push(arguments); }; ga.l = +new Date;
+    var ga = ((<any>window).ga =
+      (<any>window).ga ||
+      function() {
+        (ga.q = ga.q || []).push(arguments);
+      });
+    ga.l = +new Date();
     ga('create', (<any>config).googleAnalyticsId, 'auto');
     return ga;
   }
@@ -33,4 +36,3 @@ function startAnalytics(googleAnalyticsSrv) {
 }
 
 coreModule.service('googleAnalyticsSrv', Analytics).run(startAnalytics);
-

+ 126 - 102
public/app/core/services/backend_srv.ts

@@ -11,8 +11,13 @@ export class BackendSrv {
   private noBackendCache: boolean;
 
   /** @ngInject */
-  constructor(private $http, private alertSrv, private $q, private $timeout, private contextSrv) {
-  }
+  constructor(
+    private $http,
+    private alertSrv,
+    private $q,
+    private $timeout,
+    private contextSrv
+  ) {}
 
   get(url, params?) {
     return this.request({ method: 'GET', url: url, params: params });
@@ -52,22 +57,22 @@ export class BackendSrv {
     }
 
     if (err.status === 422) {
-      this.alertSrv.set("Validation failed", data.message, "warning", 4000);
+      this.alertSrv.set('Validation failed', data.message, 'warning', 4000);
       throw data;
     }
 
     data.severity = 'error';
 
     if (err.status < 500) {
-      data.severity = "warning";
+      data.severity = 'warning';
     }
 
     if (data.message) {
-      let description = "";
+      let description = '';
       let message = data.message;
       if (message.length > 80) {
         description = message;
-        message = "Error";
+        message = 'Error';
       }
       this.alertSrv.set(message, description, data.severity, 10000);
     }
@@ -86,32 +91,39 @@ export class BackendSrv {
         options.headers['X-Grafana-Org-Id'] = this.contextSrv.user.orgId;
       }
 
-      if (options.url.indexOf("/") === 0) {
+      if (options.url.indexOf('/') === 0) {
         options.url = options.url.substring(1);
       }
     }
 
-    return this.$http(options).then(results => {
-      if (options.method !== 'GET') {
-        if (results && results.data.message) {
-          if (options.showSuccessAlert !== false) {
-            this.alertSrv.set(results.data.message, '', 'success', 3000);
+    return this.$http(options).then(
+      results => {
+        if (options.method !== 'GET') {
+          if (results && results.data.message) {
+            if (options.showSuccessAlert !== false) {
+              this.alertSrv.set(results.data.message, '', 'success', 3000);
+            }
           }
         }
-      }
-      return results.data;
-    }, err => {
-      // handle unauthorized
-      if (err.status === 401 && this.contextSrv.user.isSignedIn && firstAttempt) {
-        return this.loginPing().then(() => {
-          options.retry = 1;
-          return this.request(options);
-        });
-      }
+        return results.data;
+      },
+      err => {
+        // handle unauthorized
+        if (
+          err.status === 401 &&
+          this.contextSrv.user.isSignedIn &&
+          firstAttempt
+        ) {
+          return this.loginPing().then(() => {
+            options.retry = 1;
+            return this.request(options);
+          });
+        }
 
-      this.$timeout(this.requestErrorHandler.bind(this, err), 50);
-      throw err;
-    });
+        this.$timeout(this.requestErrorHandler.bind(this, err), 50);
+        throw err;
+      }
+    );
   }
 
   addCanceler(requestId, canceler) {
@@ -154,7 +166,7 @@ export class BackendSrv {
         options.headers['X-Grafana-Org-Id'] = this.contextSrv.user.orgId;
       }
 
-      if (options.url.indexOf("/") === 0) {
+      if (options.url.indexOf('/') === 0) {
         options.url = options.url.substring(1);
       }
 
@@ -168,51 +180,53 @@ export class BackendSrv {
       }
     }
 
-    return this.$http(options).then(response => {
-      appEvents.emit('ds-request-response', response);
-      return response;
-    }).catch(err => {
-      if (err.status === this.HTTP_REQUEST_CANCELLED) {
-        throw {err, cancelled: true};
-      }
-
-      // handle unauthorized for backend requests
-      if (requestIsLocal && firstAttempt && err.status === 401) {
-        return this.loginPing().then(() => {
-          options.retry = 1;
-          if (canceler) {
-            canceler.resolve();
-          }
-          return this.datasourceRequest(options);
-        });
-      }
+    return this.$http(options)
+      .then(response => {
+        appEvents.emit('ds-request-response', response);
+        return response;
+      })
+      .catch(err => {
+        if (err.status === this.HTTP_REQUEST_CANCELLED) {
+          throw { err, cancelled: true };
+        }
 
-      // populate error obj on Internal Error
-      if (_.isString(err.data) && err.status === 500) {
-        err.data = {
-          error: err.statusText,
-          response: err.data,
-        };
-      }
+        // handle unauthorized for backend requests
+        if (requestIsLocal && firstAttempt && err.status === 401) {
+          return this.loginPing().then(() => {
+            options.retry = 1;
+            if (canceler) {
+              canceler.resolve();
+            }
+            return this.datasourceRequest(options);
+          });
+        }
 
-      // for Prometheus
-      if (err.data && !err.data.message && _.isString(err.data.error)) {
-        err.data.message = err.data.error;
-      }
+        // populate error obj on Internal Error
+        if (_.isString(err.data) && err.status === 500) {
+          err.data = {
+            error: err.statusText,
+            response: err.data,
+          };
+        }
 
-      appEvents.emit('ds-request-error', err);
-      throw err;
+        // for Prometheus
+        if (err.data && !err.data.message && _.isString(err.data.error)) {
+          err.data.message = err.data.error;
+        }
 
-    }).finally(() => {
-      // clean up
-      if (options.requestId) {
-        this.inFlightRequests[options.requestId].shift();
-      }
-    });
+        appEvents.emit('ds-request-error', err);
+        throw err;
+      })
+      .finally(() => {
+        // clean up
+        if (options.requestId) {
+          this.inFlightRequests[options.requestId].shift();
+        }
+      });
   }
 
   loginPing() {
-    return this.request({url: '/api/login/ping', method: 'GET', retry: 1 });
+    return this.request({ url: '/api/login/ping', method: 'GET', retry: 1 });
   }
 
   search(query) {
@@ -224,7 +238,7 @@ export class BackendSrv {
   }
 
   saveDashboard(dash, options) {
-    options = (options || {});
+    options = options || {};
 
     return this.post('/api/dashboards/db/', {
       dashboard: dash,
@@ -237,13 +251,16 @@ export class BackendSrv {
   createDashboardFolder(name) {
     const dash = {
       schemaVersion: 16,
-      title: name,
+      title: name.trim(),
       editable: true,
-      panels: []
+      panels: [],
     };
 
-    return this.post('/api/dashboards/db/', {dashboard: dash, isFolder: true, overwrite: false})
-    .then(res => {
+    return this.post('/api/dashboards/db/', {
+      dashboard: dash,
+      isFolder: true,
+      overwrite: false,
+    }).then(res => {
       return this.getDashboard('db', res.slug);
     });
   }
@@ -251,15 +268,15 @@ export class BackendSrv {
   deleteDashboard(slug) {
     let deferred = this.$q.defer();
 
-    this.getDashboard('db', slug)
-      .then(fullDash => {
-        this.delete(`/api/dashboards/db/${slug}`)
-          .then(() => {
-            deferred.resolve(fullDash);
-          }).catch(err => {
-            deferred.reject(err);
-          });
-      });
+    this.getDashboard('db', slug).then(fullDash => {
+      this.delete(`/api/dashboards/db/${slug}`)
+        .then(() => {
+          deferred.resolve(fullDash);
+        })
+        .catch(err => {
+          deferred.reject(err);
+        });
+    });
 
     return deferred.promise;
   }
@@ -278,17 +295,19 @@ export class BackendSrv {
     const tasks = [];
 
     for (let slug of dashboardSlugs) {
-      tasks.push(this.createTask(this.moveDashboard.bind(this), true, slug, toFolder));
+      tasks.push(
+        this.createTask(this.moveDashboard.bind(this), true, slug, toFolder)
+      );
     }
 
-    return this.executeInOrder(tasks, [])
-      .then(result => {
-        return {
-          totalCount: result.length,
-          successCount: _.filter(result, { succeeded: true }).length,
-          alreadyInFolderCount: _.filter(result, { alreadyInFolder: true }).length
-        };
-      });
+    return this.executeInOrder(tasks, []).then(result => {
+      return {
+        totalCount: result.length,
+        successCount: _.filter(result, { succeeded: true }).length,
+        alreadyInFolderCount: _.filter(result, { alreadyInFolder: true })
+          .length,
+      };
+    });
   }
 
   private moveDashboard(slug, toFolder) {
@@ -297,9 +316,11 @@ export class BackendSrv {
     this.getDashboard('db', slug).then(fullDash => {
       const model = new DashboardModel(fullDash.dashboard, fullDash.meta);
 
-      if ((!fullDash.meta.folderId && toFolder.id === 0) ||
-        fullDash.meta.folderId === toFolder.id) {
-        deferred.resolve({alreadyInFolder: true});
+      if (
+        (!fullDash.meta.folderId && toFolder.id === 0) ||
+        fullDash.meta.folderId === toFolder.id
+      ) {
+        deferred.resolve({ alreadyInFolder: true });
         return;
       }
 
@@ -310,19 +331,21 @@ export class BackendSrv {
 
       this.saveDashboard(clone, {})
         .then(() => {
-          deferred.resolve({succeeded: true});
-        }).catch(err => {
-          if (err.data && err.data.status === "plugin-dashboard") {
+          deferred.resolve({ succeeded: true });
+        })
+        .catch(err => {
+          if (err.data && err.data.status === 'plugin-dashboard') {
             err.isHandled = true;
 
-            this.saveDashboard(clone, {overwrite: true})
+            this.saveDashboard(clone, { overwrite: true })
               .then(() => {
-                deferred.resolve({succeeded: true});
-              }).catch(err => {
-                deferred.resolve({succeeded: false});
+                deferred.resolve({ succeeded: true });
+              })
+              .catch(err => {
+                deferred.resolve({ succeeded: false });
               });
           } else {
-            deferred.resolve({succeeded: false});
+            deferred.resolve({ succeeded: false });
           }
         });
     });
@@ -331,11 +354,13 @@ export class BackendSrv {
   }
 
   private createTask(fn, ignoreRejections, ...args: any[]) {
-    return (result) => {
-      return fn.apply(null, args)
+    return result => {
+      return fn
+        .apply(null, args)
         .then(res => {
           return Array.prototype.concat(result, [res]);
-        }).catch(err => {
+        })
+        .catch(err => {
           if (ignoreRejections) {
             return result;
           }
@@ -350,5 +375,4 @@ export class BackendSrv {
   }
 }
 
-
 coreModule.service('backendSrv', BackendSrv);

+ 6 - 3
public/app/core/services/context_srv.ts

@@ -36,7 +36,7 @@ export class ContextSrv {
       config.buildInfo = {};
     }
     if (!config.bootData) {
-      config.bootData = {user: {}, settings: {}};
+      config.bootData = { user: {}, settings: {} };
     }
 
     this.version = config.buildInfo.version;
@@ -51,7 +51,10 @@ export class ContextSrv {
   }
 
   isGrafanaVisible() {
-    return !!(document.visibilityState === undefined || document.visibilityState === 'visible');
+    return !!(
+      document.visibilityState === undefined ||
+      document.visibilityState === 'visible'
+    );
   }
 
   toggleSideMenu() {
@@ -61,7 +64,7 @@ export class ContextSrv {
 }
 
 var contextSrv = new ContextSrv();
-export {contextSrv};
+export { contextSrv };
 
 coreModule.factory('contextSrv', function() {
   return contextSrv;

+ 24 - 18
public/app/core/services/dynamic_directive_srv.ts

@@ -4,7 +4,6 @@ import angular from 'angular';
 import coreModule from '../core_module';
 
 class DynamicDirectiveSrv {
-
   /** @ngInject */
   constructor(private $compile, private $rootScope) {}
 
@@ -17,22 +16,31 @@ class DynamicDirectiveSrv {
   }
 
   link(scope, elem, attrs, options) {
-    options.directive(scope).then(directiveInfo => {
-      if (!directiveInfo || !directiveInfo.fn) {
-        elem.empty();
-        return;
-      }
+    options
+      .directive(scope)
+      .then(directiveInfo => {
+        if (!directiveInfo || !directiveInfo.fn) {
+          elem.empty();
+          return;
+        }
 
-      if (!directiveInfo.fn.registered) {
-        coreModule.directive(attrs.$normalize(directiveInfo.name), directiveInfo.fn);
-        directiveInfo.fn.registered = true;
-      }
+        if (!directiveInfo.fn.registered) {
+          coreModule.directive(
+            attrs.$normalize(directiveInfo.name),
+            directiveInfo.fn
+          );
+          directiveInfo.fn.registered = true;
+        }
 
-      this.addDirective(elem, directiveInfo.name, scope);
-    }).catch(err => {
-      console.log('Plugin load:', err);
-      this.$rootScope.appEvent('alert-error', ['Plugin error', err.toString()]);
-    });
+        this.addDirective(elem, directiveInfo.name, scope);
+      })
+      .catch(err => {
+        console.log('Plugin load:', err);
+        this.$rootScope.appEvent('alert-error', [
+          'Plugin error',
+          err.toString(),
+        ]);
+      });
   }
 
   create(options) {
@@ -52,7 +60,7 @@ class DynamicDirectiveSrv {
         } else {
           this.link(scope, elem, attrs, options);
         }
-      }
+      },
     };
 
     return directiveDef;
@@ -60,5 +68,3 @@ class DynamicDirectiveSrv {
 }
 
 coreModule.service('dynamicDirectiveSrv', DynamicDirectiveSrv);
-
-

+ 28 - 4
public/app/core/services/global_event_srv.ts

@@ -1,19 +1,43 @@
 import coreModule from 'app/core/core_module';
+import config from 'app/core/config';
 import appEvents from 'app/core/app_events';
 
 // This service is for registering global events.
 // Good for communication react > angular and vice verse
 export class GlobalEventSrv {
+  private appSubUrl;
+  private fullPageReloadRoutes;
 
   /** @ngInject */
-  constructor(private $location, private $timeout) {
+  constructor(private $location, private $timeout, private $window) {
+    this.appSubUrl = config.appSubUrl;
+    this.fullPageReloadRoutes = ['/logout'];
+  }
+
+  // Angular's $location does not like <base href...> and absolute urls
+  stripBaseFromUrl(url = '') {
+    const appSubUrl = this.appSubUrl;
+    const stripExtraChars = appSubUrl.endsWith('/') ? 1 : 0;
+    const urlWithoutBase =
+      url.length > 0 && url.indexOf(appSubUrl) === 0
+        ? url.slice(appSubUrl.length - stripExtraChars)
+        : url;
+
+    return urlWithoutBase;
   }
 
   init() {
     appEvents.on('location-change', payload => {
-        this.$timeout(() => { // A hack to use timeout when we're changing things (in this case the url) from outside of Angular.
-            this.$location.path(payload.href);
-        });
+      const urlWithoutBase = this.stripBaseFromUrl(payload.href);
+      if (this.fullPageReloadRoutes.indexOf(urlWithoutBase) > -1) {
+        this.$window.location.href = payload.href;
+        return;
+      }
+
+      this.$timeout(() => {
+        // A hack to use timeout when we're changing things (in this case the url) from outside of Angular.
+        this.$location.url(urlWithoutBase);
+      });
     });
   }
 }

+ 3 - 3
public/app/core/services/impression_srv.ts

@@ -15,7 +15,7 @@ export class ImpressionSrv {
       }
     }
 
-    impressions = impressions.filter((imp) => {
+    impressions = impressions.filter(imp => {
       return dashboardId !== imp;
     });
 
@@ -28,7 +28,7 @@ export class ImpressionSrv {
   }
 
   getDashboardOpened() {
-    var impressions = store.get(this.impressionKey(config)) || "[]";
+    var impressions = store.get(this.impressionKey(config)) || '[]';
 
     impressions = JSON.parse(impressions);
 
@@ -40,7 +40,7 @@ export class ImpressionSrv {
   }
 
   impressionKey(config) {
-    return "dashboard_impressions-" + config.bootData.user.orgId;
+    return 'dashboard_impressions-' + config.bootData.user.orgId;
   }
 }
 

+ 28 - 27
public/app/core/services/keybindingSrv.ts

@@ -10,10 +10,7 @@ export class KeybindingSrv {
   helpModal: boolean;
 
   /** @ngInject */
-  constructor(
-    private $rootScope,
-    private $location) {
-
+  constructor(private $rootScope, private $location) {
     // clear out all shortcuts on route change
     $rootScope.$on('$routeChangeSuccess', () => {
       Mousetrap.reset();
@@ -26,54 +23,58 @@ export class KeybindingSrv {
 
   setupGlobal() {
     this.bind(['?', 'h'], this.showHelpModal);
-    this.bind("g h", this.goToHome);
-    this.bind("g a", this.openAlerting);
-    this.bind("g p", this.goToProfile);
-    this.bind("s s", this.openSearchStarred);
+    this.bind('g h', this.goToHome);
+    this.bind('g a', this.openAlerting);
+    this.bind('g p', this.goToProfile);
+    this.bind('s s', this.openSearchStarred);
     this.bind('s o', this.openSearch);
     this.bind('s t', this.openSearchTags);
     this.bind('f', this.openSearch);
   }
 
   openSearchStarred() {
-    this.$rootScope.appEvent('show-dash-search', {starred: true});
+    appEvents.emit('show-dash-search', { starred: true });
   }
 
   openSearchTags() {
-    this.$rootScope.appEvent('show-dash-search', {tagsMode: true});
+    appEvents.emit('show-dash-search', { tagsMode: true });
   }
 
   openSearch() {
-    this.$rootScope.appEvent('show-dash-search');
+    appEvents.emit('show-dash-search');
   }
 
   openAlerting() {
-    this.$location.url("/alerting");
+    this.$location.url('/alerting');
   }
 
   goToHome() {
-    this.$location.url("/");
+    this.$location.url('/');
   }
 
   goToProfile() {
-    this.$location.url("/profile");
+    this.$location.url('/profile');
   }
 
   showHelpModal() {
-    appEvents.emit('show-modal', {templateHtml: '<help-modal></help-modal>'});
+    appEvents.emit('show-modal', { templateHtml: '<help-modal></help-modal>' });
   }
 
   bind(keyArg, fn) {
-    Mousetrap.bind(keyArg, evt => {
-      evt.preventDefault();
-      evt.stopPropagation();
-      evt.returnValue = false;
-      return this.$rootScope.$apply(fn.bind(this));
-    }, 'keydown');
+    Mousetrap.bind(
+      keyArg,
+      evt => {
+        evt.preventDefault();
+        evt.stopPropagation();
+        evt.returnValue = false;
+        return this.$rootScope.$apply(fn.bind(this));
+      },
+      'keydown'
+    );
   }
 
   showDashEditView() {
-    var search = _.extend(this.$location.search(), {editview: 'settings'});
+    var search = _.extend(this.$location.search(), { editview: 'settings' });
     this.$location.search(search);
   }
 
@@ -111,7 +112,7 @@ export class KeybindingSrv {
           fullscreen: true,
           edit: true,
           panelId: dashboard.meta.focusPanelId,
-          toggle: true
+          toggle: true,
         });
       }
     });
@@ -140,14 +141,14 @@ export class KeybindingSrv {
     // share panel
     this.bind('p s', () => {
       if (dashboard.meta.focusPanelId) {
-        var shareScope =  scope.$new();
+        var shareScope = scope.$new();
         var panelInfo = dashboard.getPanelInfoById(dashboard.meta.focusPanelId);
         shareScope.panel = panelInfo.panel;
         shareScope.dashboard = dashboard;
 
         appEvents.emit('show-modal', {
           src: 'public/app/features/dashboard/partials/shareModal.html',
-          scope: shareScope
+          scope: shareScope,
         });
       }
     });
@@ -185,7 +186,7 @@ export class KeybindingSrv {
     });
 
     this.bind('d n', e => {
-      this.$location.url("/dashboard/new");
+      this.$location.url('/dashboard/new');
     });
 
     this.bind('d r', () => {
@@ -211,7 +212,7 @@ export class KeybindingSrv {
       }
 
       scope.appEvent('hide-modal');
-      scope.appEvent('panel-change-view', {fullscreen: false, edit: false});
+      scope.appEvent('panel-change-view', { fullscreen: false, edit: false });
 
       // close settings view
       var search = this.$location.search();

+ 39 - 19
public/app/core/services/ng_react.ts

@@ -2,7 +2,6 @@
 // This is using ng-react with this PR applied https://github.com/ngReact/ngReact/pull/199
 //
 
-
 // # ngReact
 // ### Use React Components inside of your Angular applications
 //
@@ -85,24 +84,27 @@ function applyFunctions(obj, scope, propsConfig?) {
     var value = obj[key];
     var config = (propsConfig || {})[key] || {};
     /**
-       * wrap functions in a function that ensures they are scope.$applied
-       * ensures that when function is called from a React component
-       * the Angular digest cycle is run
-       */
-    prev[key] = angular.isFunction(value) && config.wrapApply !== false ? applied(value, scope) : value;
+     * wrap functions in a function that ensures they are scope.$applied
+     * ensures that when function is called from a React component
+     * the Angular digest cycle is run
+     */
+    prev[key] =
+      angular.isFunction(value) && config.wrapApply !== false
+        ? applied(value, scope)
+        : value;
 
     return prev;
   }, {});
 }
 
 /**
-   *
-   * @param watchDepth (value of HTML watch-depth attribute)
-   * @param scope (angular scope)
-   *
-   * Uses the watchDepth attribute to determine how to watch props on scope.
-   * If watchDepth attribute is NOT reference or collection, watchDepth defaults to deep watching by value
-   */
+ *
+ * @param watchDepth (value of HTML watch-depth attribute)
+ * @param scope (angular scope)
+ *
+ * Uses the watchDepth attribute to determine how to watch props on scope.
+ * If watchDepth attribute is NOT reference or collection, watchDepth defaults to deep watching by value
+ */
 function watchProps(watchDepth, scope, watchExpressions, listener) {
   var supportsWatchCollection = angular.isFunction(scope.$watchCollection);
   var supportsWatchGroup = angular.isFunction(scope.$watchGroup);
@@ -165,7 +167,8 @@ function findAttribute(attrs, propName) {
 
 // get watch depth of prop (string or array)
 function getPropWatchDepth(defaultWatch, prop) {
-  var customWatchDepth = Array.isArray(prop) && angular.isObject(prop[1]) && prop[1].watchDepth;
+  var customWatchDepth =
+    Array.isArray(prop) && angular.isObject(prop[1]) && prop[1].watchDepth;
   return customWatchDepth || defaultWatch;
 }
 
@@ -202,7 +205,9 @@ var reactComponent = function($injector) {
       };
 
       // If there are props, re-render when they change
-      attrs.props ? watchProps(attrs.watchDepth, scope, [attrs.props], renderMyComponent) : renderMyComponent();
+      attrs.props
+        ? watchProps(attrs.watchDepth, scope, [attrs.props], renderMyComponent)
+        : renderMyComponent();
 
       // cleanup when scope is destroyed
       scope.$on('$destroy', function() {
@@ -210,7 +215,10 @@ var reactComponent = function($injector) {
           ReactDOM.unmountComponentAtNode(elem[0]);
         } else {
           scope.$eval(attrs.onScopeDestroy, {
-            unmountComponent: ReactDOM.unmountComponentAtNode.bind(this, elem[0]),
+            unmountComponent: ReactDOM.unmountComponentAtNode.bind(
+              this,
+              elem[0]
+            ),
           });
         }
       });
@@ -274,11 +282,20 @@ var reactDirective = function($injector) {
         // watch each property name and trigger an update whenever something changes,
         // to update scope.props with new values
         var propExpressions = props.map(function(prop) {
-          return Array.isArray(prop) ? [attrs[getPropName(prop)], getPropConfig(prop)] : attrs[prop];
+          return Array.isArray(prop)
+            ? [attrs[getPropName(prop)], getPropConfig(prop)]
+            : attrs[prop];
         });
 
         // If we don't have any props, then our watch statement won't fire.
-        props.length ? watchProps(attrs.watchDepth, scope, propExpressions, renderMyComponent) : renderMyComponent();
+        props.length
+          ? watchProps(
+              attrs.watchDepth,
+              scope,
+              propExpressions,
+              renderMyComponent
+            )
+          : renderMyComponent();
 
         // cleanup when scope is destroyed
         scope.$on('$destroy', function() {
@@ -286,7 +303,10 @@ var reactDirective = function($injector) {
             ReactDOM.unmountComponentAtNode(elem[0]);
           } else {
             scope.$eval(attrs.onScopeDestroy, {
-              unmountComponent: ReactDOM.unmountComponentAtNode.bind(this, elem[0]),
+              unmountComponent: ReactDOM.unmountComponentAtNode.bind(
+                this,
+                elem[0]
+              ),
             });
           }
         });

+ 2 - 2
public/app/core/services/popover_srv.ts

@@ -57,8 +57,8 @@ function popoverSrv($compile, $rootScope, $timeout) {
         openOn: options.openOn,
         hoverCloseDelay: 200,
         tetherOptions: {
-          constraints: [{to: 'scrollParent', attachment: "none both"}]
-        }
+          constraints: [{ to: 'scrollParent', attachment: 'together' }],
+        },
       });
 
       drop.on('close', () => {

+ 17 - 11
public/app/core/services/search_srv.ts

@@ -18,7 +18,7 @@ export class SearchSrv {
     return this.queryForRecentDashboards().then(result => {
       if (result.length > 0) {
         sections['recent'] = {
-          title: 'Recent Boards',
+          title: 'Recent',
           icon: 'fa fa-clock-o',
           score: -1,
           removable: true,
@@ -37,9 +37,11 @@ export class SearchSrv {
     }
 
     return this.backendSrv.search({ dashboardIds: dashIds }).then(result => {
-      return dashIds.map(orderId => {
-        return _.find(result, { id: orderId });
-      }).filter(hit => hit && !hit.isStarred)
+      return dashIds
+        .map(orderId => {
+          return _.find(result, { id: orderId });
+        })
+        .filter(hit => hit && !hit.isStarred)
         .map(hit => {
           return this.transformToViewModel(hit);
         });
@@ -71,10 +73,10 @@ export class SearchSrv {
       return Promise.resolve();
     }
 
-    return this.backendSrv.search({starred: true, limit: 5}).then(result => {
+    return this.backendSrv.search({ starred: true, limit: 5 }).then(result => {
       if (result.length > 0) {
         sections['starred'] = {
-          title: 'Starred Boards',
+          title: 'Starred',
           icon: 'fa fa-star-o',
           score: -2,
           expanded: this.starredIsOpen,
@@ -94,8 +96,10 @@ export class SearchSrv {
     let sections: any = {};
     let promises = [];
     let query = _.clone(options);
-    let hasFilters = options.query ||
-      (options.tag && options.tag.length > 0) || options.starred ||
+    let hasFilters =
+      options.query ||
+      (options.tag && options.tag.length > 0) ||
+      options.starred ||
       (options.folderIds && options.folderIds.length > 0);
 
     if (!options.skipRecent && !hasFilters) {
@@ -111,9 +115,11 @@ export class SearchSrv {
       query.folderIds = [0];
     }
 
-    promises.push(this.backendSrv.search(query).then(results => {
-      return this.handleSearchResult(sections, results);
-    }));
+    promises.push(
+      this.backendSrv.search(query).then(results => {
+        return this.handleSearchResult(sections, results);
+      })
+    );
 
     return this.$q.all(promises).then(() => {
       return _.sortBy(_.values(sections), 'score');

+ 1 - 2
public/app/core/services/timer.ts

@@ -7,8 +7,7 @@ export class Timer {
   timers = [];
 
   /** @ngInject */
-  constructor(private $timeout) {
-  }
+  constructor(private $timeout) {}
 
   register(promise) {
     this.timers.push(promise);

+ 2 - 3
public/app/core/services/util_srv.ts

@@ -7,8 +7,7 @@ export class UtilSrv {
   modalScope: any;
 
   /** @ngInject */
-  constructor(private $rootScope, private $modal) {
-  }
+  constructor(private $rootScope, private $modal) {}
 
   init() {
     appEvents.on('show-modal', this.showModal.bind(this), this.$rootScope);
@@ -43,7 +42,7 @@ export class UtilSrv {
       show: false,
       scope: this.modalScope,
       keyboard: false,
-      backdrop: options.backdrop
+      backdrop: options.backdrop,
     });
 
     Promise.resolve(modal).then(function(modalEl) {

+ 21 - 11
public/app/core/specs/backend_srv_specs.ts

@@ -1,4 +1,10 @@
-import {describe, beforeEach, it, expect, angularMocks} from 'test/lib/common';
+import {
+  describe,
+  beforeEach,
+  it,
+  expect,
+  angularMocks,
+} from 'test/lib/common';
 import 'app/core/services/backend_srv';
 
 describe('backend_srv', function() {
@@ -7,20 +13,24 @@ describe('backend_srv', function() {
 
   beforeEach(angularMocks.module('grafana.core'));
   beforeEach(angularMocks.module('grafana.services'));
-  beforeEach(angularMocks.inject(function ($httpBackend, $http, backendSrv) {
-    _httpBackend = $httpBackend;
-    _backendSrv = backendSrv;
-  }));
+  beforeEach(
+    angularMocks.inject(function($httpBackend, $http, backendSrv) {
+      _httpBackend = $httpBackend;
+      _backendSrv = backendSrv;
+    })
+  );
 
   describe('when handling errors', function() {
     it('should return the http status code', function(done) {
       _httpBackend.whenGET('gateway-error').respond(502);
-      _backendSrv.datasourceRequest({
-        url: 'gateway-error'
-      }).catch(function(err) {
-        expect(err.status).to.be(502);
-        done();
-      });
+      _backendSrv
+        .datasourceRequest({
+          url: 'gateway-error',
+        })
+        .catch(function(err) {
+          expect(err.status).to.be(502);
+          done();
+        });
       _httpBackend.flush();
     });
   });

+ 38 - 20
public/app/core/specs/datemath.jest.ts

@@ -4,9 +4,9 @@ import * as dateMath from 'app/core/utils/datemath';
 import moment from 'moment';
 import _ from 'lodash';
 
-describe("DateMath", () => {
+describe('DateMath', () => {
   var spans = ['s', 'm', 'h', 'd', 'w', 'M', 'y'];
-  var anchor =  '2014-01-01T06:06:06.666Z';
+  var anchor = '2014-01-01T06:06:06.666Z';
   var unix = moment(anchor).valueOf();
   var format = 'YYYY-MM-DDTHH:mm:ss.SSSZ';
   var clock;
@@ -20,9 +20,12 @@ describe("DateMath", () => {
       expect(dateMath.parse('now&1d')).toBe(undefined);
     });
 
-    it('should return undefined if I pass a unit besides' + spans.toString(), () => {
-      expect(dateMath.parse('now+5f')).toBe(undefined);
-    });
+    it(
+      'should return undefined if I pass a unit besides' + spans.toString(),
+      () => {
+        expect(dateMath.parse('now+5f')).toBe(undefined);
+      }
+    );
 
     it('should return undefined if rounding unit is not 1', () => {
       expect(dateMath.parse('now/2y')).toBe(undefined);
@@ -35,7 +38,7 @@ describe("DateMath", () => {
     });
   });
 
-  it("now/d should set to start of current day", () => {
+  it('now/d should set to start of current day', () => {
     var expected = new Date();
     expected.setHours(0);
     expected.setMinutes(0);
@@ -46,9 +49,19 @@ describe("DateMath", () => {
     expect(startOfDay).toBe(expected.getTime());
   });
 
-  it("now/d on a utc dashboard should be start of the current day in UTC time", () => {
+  it('now/d on a utc dashboard should be start of the current day in UTC time', () => {
     var today = new Date();
-    var expected = new Date(Date.UTC(today.getUTCFullYear(), today.getUTCMonth(), today.getUTCDate(), 0, 0, 0, 0));
+    var expected = new Date(
+      Date.UTC(
+        today.getUTCFullYear(),
+        today.getUTCMonth(),
+        today.getUTCDate(),
+        0,
+        0,
+        0,
+        0
+      )
+    );
 
     var startOfDay = dateMath.parse('now/d', false, 'utc').valueOf();
     expect(startOfDay).toBe(expected.getTime());
@@ -64,16 +77,20 @@ describe("DateMath", () => {
       anchored = moment(anchor);
     });
 
-    _.each(spans, (span) => {
+    _.each(spans, span => {
       var nowEx = 'now-5' + span;
-      var thenEx =  anchor + '||-5' + span;
+      var thenEx = anchor + '||-5' + span;
 
       it('should return 5' + span + ' ago', () => {
-        expect(dateMath.parse(nowEx).format(format)).toEqual(now.subtract(5, span).format(format));
+        expect(dateMath.parse(nowEx).format(format)).toEqual(
+          now.subtract(5, span).format(format)
+        );
       });
 
       it('should return 5' + span + ' before ' + anchor, () => {
-        expect(dateMath.parse(thenEx).format(format)).toEqual(anchored.subtract(5, span).format(format));
+        expect(dateMath.parse(thenEx).format(format)).toEqual(
+          anchored.subtract(5, span).format(format)
+        );
       });
     });
 
@@ -90,13 +107,17 @@ describe("DateMath", () => {
       now = moment();
     });
 
-    _.each(spans, (span) => {
-      it('should round now to the beginning of the ' + span, function () {
-        expect(dateMath.parse('now/' + span).format(format)).toEqual(now.startOf(span).format(format));
+    _.each(spans, span => {
+      it('should round now to the beginning of the ' + span, function() {
+        expect(dateMath.parse('now/' + span).format(format)).toEqual(
+          now.startOf(span).format(format)
+        );
       });
 
-      it('should round now to the end of the ' + span, function () {
-        expect(dateMath.parse('now/' + span, true).format(format)).toEqual(now.endOf(span).format(format));
+      it('should round now to the end of the ' + span, function() {
+        expect(dateMath.parse('now/' + span, true).format(format)).toEqual(
+          now.endOf(span).format(format)
+        );
       });
     });
 
@@ -130,7 +151,4 @@ describe("DateMath", () => {
       expect(date).toEqual(undefined);
     });
   });
-
 });
-
-

+ 9 - 9
public/app/core/specs/emitter.jest.ts

@@ -1,9 +1,7 @@
-import {Emitter} from '../utils/emitter';
-
-describe("Emitter", () => {
+import { Emitter } from '../utils/emitter';
 
+describe('Emitter', () => {
   describe('given 2 subscribers', () => {
-
     it('should notfiy subscribers', () => {
       var events = new Emitter();
       var sub1Called = false;
@@ -45,20 +43,22 @@ describe("Emitter", () => {
 
       events.on('test', () => {
         sub1Called++;
-        throw {message: "hello"};
+        throw { message: 'hello' };
       });
 
       events.on('test', () => {
         sub2Called++;
       });
 
-      try { events.emit('test', null); } catch (_) { }
-      try { events.emit('test', null); } catch (_) {}
+      try {
+        events.emit('test', null);
+      } catch (_) {}
+      try {
+        events.emit('test', null);
+      } catch (_) {}
 
       expect(sub1Called).toBe(2);
       expect(sub2Called).toBe(0);
     });
   });
 });
-
-

+ 12 - 12
public/app/core/specs/flatten.jest.ts

@@ -1,22 +1,22 @@
 import flatten from 'app/core/utils/flatten';
 
-describe("flatten", () => {
-
+describe('flatten', () => {
   it('should return flatten object', () => {
-    var flattened = flatten({
-      level1: 'level1-value',
-      deeper: {
-        level2: 'level2-value',
+    var flattened = flatten(
+      {
+        level1: 'level1-value',
         deeper: {
-          level3: 'level3-value'
-        }
-      }
-    }, null);
+          level2: 'level2-value',
+          deeper: {
+            level3: 'level3-value',
+          },
+        },
+      },
+      null
+    );
 
     expect(flattened['level1']).toBe('level1-value');
     expect(flattened['deeper.level2']).toBe('level2-value');
     expect(flattened['deeper.deeper.level3']).toBe('level3-value');
   });
-
 });
-

+ 23 - 0
public/app/core/specs/global_event_srv.jest.ts

@@ -0,0 +1,23 @@
+import { GlobalEventSrv } from 'app/core/services/global_event_srv';
+import { beforeEach } from 'test/lib/common';
+
+jest.mock('app/core/config', () => {
+  return {
+    appSubUrl: '/subUrl',
+  };
+});
+
+describe('GlobalEventSrv', () => {
+  let searchSrv;
+
+  beforeEach(() => {
+    searchSrv = new GlobalEventSrv(null, null, null);
+  });
+
+  describe('With /subUrl as appSubUrl', () => {
+    it('/subUrl should be stripped', () => {
+      const urlWithoutMaster = searchSrv.stripBaseFromUrl('/subUrl/grafana/');
+      expect(urlWithoutMaster).toBe('/grafana/');
+    });
+  });
+});

+ 43 - 35
public/app/core/specs/kbn.jest.ts

@@ -5,9 +5,7 @@ import moment from 'moment';
 describe('unit format menu', function() {
   var menu = kbn.getUnitFormats();
   menu.map(function(submenu) {
-
     describe('submenu ' + submenu.text, function() {
-
       it('should have a title', function() {
         expect(typeof submenu.text).toBe('string');
       });
@@ -18,8 +16,12 @@ describe('unit format menu', function() {
 
       submenu.submenu.map(function(entry) {
         describe('entry ' + entry.text, function() {
-          it('should have a title', function() { expect(typeof entry.text).toBe('string'); });
-          it('should have a format', function() { expect(typeof entry.value).toBe('string'); });
+          it('should have a title', function() {
+            expect(typeof entry.text).toBe('string');
+          });
+          it('should have a format', function() {
+            expect(typeof entry.value).toBe('string');
+          });
           it('should have a valid format', function() {
             expect(typeof kbn.valueFormats[entry.value]).toBe('function');
           });
@@ -30,15 +32,14 @@ describe('unit format menu', function() {
 });
 
 function describeValueFormat(desc, value, tickSize, tickDecimals, result) {
-
   describe('value format: ' + desc, function() {
     it('should translate ' + value + ' as ' + result, function() {
-      var scaledDecimals = tickDecimals - Math.floor(Math.log(tickSize) / Math.LN10);
+      var scaledDecimals =
+        tickDecimals - Math.floor(Math.log(tickSize) / Math.LN10);
       var str = kbn.valueFormats[desc](value, tickDecimals, scaledDecimals);
       expect(str).toBe(result);
     });
   });
-
 }
 
 describeValueFormat('ms', 0.0024, 0.0005, 4, '0.0024 ms');
@@ -53,7 +54,7 @@ describeValueFormat('none', 2.75e-10, 0, 10, '3e-10');
 describeValueFormat('none', 0, 0, 2, '0');
 describeValueFormat('dB', 10, 1000, 2, '10.00 dB');
 
-describeValueFormat('percent',  0, 0, 0, '0%');
+describeValueFormat('percent', 0, 0, 0, '0%');
 describeValueFormat('percent', 53, 0, 1, '53.0%');
 describeValueFormat('percentunit', 0.0, 0, 0, '0%');
 describeValueFormat('percentunit', 0.278, 0, 1, '27.8%');
@@ -63,7 +64,7 @@ describeValueFormat('currencyUSD', 7.42, 10000, 2, '$7.42');
 describeValueFormat('currencyUSD', 1532.82, 1000, 1, '$1.53K');
 describeValueFormat('currencyUSD', 18520408.7, 10000000, 0, '$19M');
 
-describeValueFormat('bytes', -1.57e+308, -1.57e+308, 2, 'NA');
+describeValueFormat('bytes', -1.57e308, -1.57e308, 2, 'NA');
 
 describeValueFormat('ns', 25, 1, 0, '25 ns');
 describeValueFormat('ns', 2558, 50, 0, '2.56 µs');
@@ -109,7 +110,7 @@ describe('date time formats', function() {
   it('should format as iso date and skip date when today', function() {
     var now = moment();
     var str = kbn.valueFormats.dateTimeAsIso(now.valueOf(), 1);
-    expect(str).toBe(now.format("HH:mm:ss"));
+    expect(str).toBe(now.format('HH:mm:ss'));
   });
 
   it('should format as US date', function() {
@@ -120,7 +121,7 @@ describe('date time formats', function() {
   it('should format as US date and skip date when today', function() {
     var now = moment();
     var str = kbn.valueFormats.dateTimeAsUS(now.valueOf(), 1);
-    expect(str).toBe(now.format("h:mm:ss a"));
+    expect(str).toBe(now.format('h:mm:ss a'));
   });
 
   it('should format as from now with days', function() {
@@ -190,7 +191,7 @@ describe('calculateInterval', function() {
   });
 
   it('fixed user min interval', function() {
-    var range = {from: dateMath.parse('now-10m'), to: dateMath.parse('now')};
+    var range = { from: dateMath.parse('now-10m'), to: dateMath.parse('now') };
     var res = kbn.calculateInterval(range, 1600, '10s');
     expect(res.interval).toBe('10s');
     expect(res.intervalMs).toBe(10000);
@@ -203,7 +204,7 @@ describe('calculateInterval', function() {
   });
 
   it('large time range and user low limit', function() {
-    var range = {from: dateMath.parse('now-14d'), to: dateMath.parse('now')};
+    var range = { from: dateMath.parse('now-14d'), to: dateMath.parse('now') };
     var res = kbn.calculateInterval(range, 1000, '>10s');
     expect(res.interval).toBe('20m');
   });
@@ -222,7 +223,10 @@ describe('calculateInterval', function() {
   });
 
   it('86399s 1 resolution', function() {
-    var range = { from: dateMath.parse('now-86390s'), to: dateMath.parse('now') };
+    var range = {
+      from: dateMath.parse('now-86390s'),
+      to: dateMath.parse('now'),
+    };
     var res = kbn.calculateInterval(range, 1, null);
     expect(res.interval).toBe('12h');
     expect(res.intervalMs).toBe(43200000);
@@ -254,11 +258,11 @@ describe('hex', function() {
 
 describe('hex 0x', function() {
   it('positive integeter', function() {
-    var str = kbn.valueFormats.hex0x(7999,0);
+    var str = kbn.valueFormats.hex0x(7999, 0);
     expect(str).toBe('0x1F3F');
   });
   it('negative integer', function() {
-    var str = kbn.valueFormats.hex0x(-584,0);
+    var str = kbn.valueFormats.hex0x(-584, 0);
     expect(str).toBe('-0x248');
   });
   it('null', function() {
@@ -277,71 +281,75 @@ describe('hex 0x', function() {
 
 describe('duration', function() {
   it('null', function() {
-    var str = kbn.toDuration(null, 0, "millisecond");
+    var str = kbn.toDuration(null, 0, 'millisecond');
     expect(str).toBe('');
   });
   it('0 milliseconds', function() {
-    var str = kbn.toDuration(0, 0, "millisecond");
+    var str = kbn.toDuration(0, 0, 'millisecond');
     expect(str).toBe('0 milliseconds');
   });
   it('1 millisecond', function() {
-    var str = kbn.toDuration(1, 0, "millisecond");
+    var str = kbn.toDuration(1, 0, 'millisecond');
     expect(str).toBe('1 millisecond');
   });
   it('-1 millisecond', function() {
-    var str = kbn.toDuration(-1, 0, "millisecond");
+    var str = kbn.toDuration(-1, 0, 'millisecond');
     expect(str).toBe('1 millisecond ago');
   });
   it('seconds', function() {
-    var str = kbn.toDuration(1, 0, "second");
+    var str = kbn.toDuration(1, 0, 'second');
     expect(str).toBe('1 second');
   });
   it('minutes', function() {
-    var str = kbn.toDuration(1, 0, "minute");
+    var str = kbn.toDuration(1, 0, 'minute');
     expect(str).toBe('1 minute');
   });
   it('hours', function() {
-    var str = kbn.toDuration(1, 0, "hour");
+    var str = kbn.toDuration(1, 0, 'hour');
     expect(str).toBe('1 hour');
   });
   it('days', function() {
-    var str = kbn.toDuration(1, 0, "day");
+    var str = kbn.toDuration(1, 0, 'day');
     expect(str).toBe('1 day');
   });
   it('weeks', function() {
-    var str = kbn.toDuration(1, 0, "week");
+    var str = kbn.toDuration(1, 0, 'week');
     expect(str).toBe('1 week');
   });
   it('months', function() {
-    var str = kbn.toDuration(1, 0, "month");
+    var str = kbn.toDuration(1, 0, 'month');
     expect(str).toBe('1 month');
   });
   it('years', function() {
-    var str = kbn.toDuration(1, 0, "year");
+    var str = kbn.toDuration(1, 0, 'year');
     expect(str).toBe('1 year');
   });
   it('decimal days', function() {
-    var str = kbn.toDuration(1.5, 2, "day");
+    var str = kbn.toDuration(1.5, 2, 'day');
     expect(str).toBe('1 day, 12 hours, 0 minutes');
   });
   it('decimal months', function() {
-    var str = kbn.toDuration(1.5, 3, "month");
+    var str = kbn.toDuration(1.5, 3, 'month');
     expect(str).toBe('1 month, 2 weeks, 1 day, 0 hours');
   });
   it('no decimals', function() {
-    var str = kbn.toDuration(38898367008, 0, "millisecond");
+    var str = kbn.toDuration(38898367008, 0, 'millisecond');
     expect(str).toBe('1 year');
   });
   it('1 decimal', function() {
-    var str = kbn.toDuration(38898367008, 1, "millisecond");
+    var str = kbn.toDuration(38898367008, 1, 'millisecond');
     expect(str).toBe('1 year, 2 months');
   });
   it('too many decimals', function() {
-    var str = kbn.toDuration(38898367008, 20, "millisecond");
-    expect(str).toBe('1 year, 2 months, 3 weeks, 4 days, 5 hours, 6 minutes, 7 seconds, 8 milliseconds');
+    var str = kbn.toDuration(38898367008, 20, 'millisecond');
+    expect(str).toBe(
+      '1 year, 2 months, 3 weeks, 4 days, 5 hours, 6 minutes, 7 seconds, 8 milliseconds'
+    );
   });
   it('floating point error', function() {
-    var str = kbn.toDuration(36993906007, 8, "millisecond");
-    expect(str).toBe('1 year, 2 months, 0 weeks, 3 days, 4 hours, 5 minutes, 6 seconds, 7 milliseconds');
+    var str = kbn.toDuration(36993906007, 8, 'millisecond');
+    expect(str).toBe(
+      '1 year, 2 months, 0 weeks, 3 days, 4 hours, 5 minutes, 6 seconds, 7 milliseconds'
+    );
   });
 });

+ 85 - 117
public/app/core/specs/manage_dashboards.jest.ts

@@ -10,43 +10,43 @@ describe('ManageDashboards', () => {
       const response = [
         {
           id: 410,
-          title: "afolder",
-          type: "dash-folder",
+          title: 'afolder',
+          type: 'dash-folder',
           items: [
             {
               id: 399,
-              title: "Dashboard Test",
-              url: "dashboard/db/dashboard-test",
+              title: 'Dashboard Test',
+              url: 'dashboard/db/dashboard-test',
               icon: 'fa fa-folder',
               tags: [],
               isStarred: false,
               folderId: 410,
-              folderTitle: "afolder",
-              folderSlug: "afolder"
-            }
+              folderTitle: 'afolder',
+              folderSlug: 'afolder',
+            },
           ],
           tags: [],
-          isStarred: false
+          isStarred: false,
         },
         {
           id: 0,
-          title: "Root",
+          title: 'Root',
           icon: 'fa fa-folder-open',
-          uri: "db/something-else",
-          type: "dash-db",
+          uri: 'db/something-else',
+          type: 'dash-db',
           items: [
             {
               id: 500,
-              title: "Dashboard Test",
-              url: "dashboard/db/dashboard-test",
+              title: 'Dashboard Test',
+              url: 'dashboard/db/dashboard-test',
               icon: 'fa fa-folder',
               tags: [],
-              isStarred: false
-            }
+              isStarred: false,
+            },
           ],
           tags: [],
           isStarred: false,
-        }
+        },
       ];
       ctrl = createCtrlWithStubs(response);
       return ctrl.getDashboards();
@@ -67,24 +67,24 @@ describe('ManageDashboards', () => {
       const response = [
         {
           id: 410,
-          title: "afolder",
-          type: "dash-folder",
+          title: 'afolder',
+          type: 'dash-folder',
           items: [
             {
               id: 399,
-              title: "Dashboard Test",
-              url: "dashboard/db/dashboard-test",
+              title: 'Dashboard Test',
+              url: 'dashboard/db/dashboard-test',
               icon: 'fa fa-folder',
               tags: [],
               isStarred: false,
               folderId: 410,
-              folderTitle: "afolder",
-              folderSlug: "afolder"
-            }
+              folderTitle: 'afolder',
+              folderSlug: 'afolder',
+            },
           ],
           tags: [],
-          isStarred: false
-        }
+          isStarred: false,
+        },
       ];
       ctrl = createCtrlWithStubs(response);
       ctrl.folderId = 410;
@@ -106,26 +106,26 @@ describe('ManageDashboards', () => {
           items: [
             {
               id: 399,
-              title: "Dashboard Test",
-              url: "dashboard/db/dashboard-test",
+              title: 'Dashboard Test',
+              url: 'dashboard/db/dashboard-test',
               icon: 'fa fa-folder',
               tags: [],
               isStarred: false,
               folderId: 410,
-              folderTitle: "afolder",
-              folderSlug: "afolder"
+              folderTitle: 'afolder',
+              folderSlug: 'afolder',
             },
             {
               id: 500,
-              title: "Dashboard Test",
-              url: "dashboard/db/dashboard-test",
+              title: 'Dashboard Test',
+              url: 'dashboard/db/dashboard-test',
               icon: 'fa fa-folder',
               tags: [],
               folderId: 499,
-              isStarred: false
-            }
-          ]
-        }
+              isStarred: false,
+            },
+          ],
+        },
       ];
 
       ctrl = createCtrlWithStubs(response);
@@ -261,18 +261,14 @@ describe('ManageDashboards', () => {
         ctrl.sections = [
           {
             id: 1,
-            items: [
-              { id: 2, checked: false }
-            ],
-            checked: false
+            items: [{ id: 2, checked: false }],
+            checked: false,
           },
           {
             id: 0,
-            items: [
-              { id: 3, checked: false }
-            ],
-            checked: false
-          }
+            items: [{ id: 3, checked: false }],
+            checked: false,
+          },
         ];
         ctrl.selectionChanged();
       });
@@ -313,18 +309,14 @@ describe('ManageDashboards', () => {
         ctrl.sections = [
           {
             id: 1,
-            items: [
-              { id: 2, checked: true }
-            ],
-            checked: true
+            items: [{ id: 2, checked: true }],
+            checked: true,
           },
           {
             id: 0,
-            items: [
-              { id: 3, checked: true }
-            ],
-            checked: true
-          }
+            items: [{ id: 3, checked: true }],
+            checked: true,
+          },
         ];
         ctrl.selectionChanged();
       });
@@ -366,19 +358,15 @@ describe('ManageDashboards', () => {
           {
             id: 1,
             title: 'folder',
-            items: [
-              { id: 2, checked: false }
-            ],
-            checked: false
+            items: [{ id: 2, checked: false }],
+            checked: false,
           },
           {
             id: 0,
             title: 'Root',
-            items: [
-              { id: 3, checked: true }
-            ],
-            checked: false
-          }
+            items: [{ id: 3, checked: true }],
+            checked: false,
+          },
         ];
         ctrl.selectionChanged();
       });
@@ -398,19 +386,15 @@ describe('ManageDashboards', () => {
           {
             id: 1,
             title: 'folder',
-            items: [
-              { id: 2, checked: true }
-            ],
-            checked: false
+            items: [{ id: 2, checked: true }],
+            checked: false,
           },
           {
             id: 0,
             title: 'Root',
-            items: [
-              { id: 3, checked: false }
-            ],
-            checked: false
-          }
+            items: [{ id: 3, checked: false }],
+            checked: false,
+          },
         ];
 
         ctrl.selectionChanged();
@@ -431,19 +415,15 @@ describe('ManageDashboards', () => {
           {
             id: 1,
             title: 'folder',
-            items: [
-              { id: 2, checked: true }
-            ],
-            checked: false
+            items: [{ id: 2, checked: true }],
+            checked: false,
           },
           {
             id: 0,
             title: 'Root',
-            items: [
-              { id: 3, checked: true }
-            ],
-            checked: false
-          }
+            items: [{ id: 3, checked: true }],
+            checked: false,
+          },
         ];
 
         ctrl.selectionChanged();
@@ -464,27 +444,21 @@ describe('ManageDashboards', () => {
           {
             id: 1,
             title: 'folder',
-            items: [
-              { id: 2, checked: false }
-            ],
-            checked: true
+            items: [{ id: 2, checked: false }],
+            checked: true,
           },
           {
             id: 3,
             title: 'folder',
-            items: [
-              { id: 4, checked: true }
-            ],
-            checked: false
+            items: [{ id: 4, checked: true }],
+            checked: false,
           },
           {
             id: 0,
             title: 'Root',
-            items: [
-              { id: 3, checked: false }
-            ],
-            checked: false
-          }
+            items: [{ id: 3, checked: false }],
+            checked: false,
+          },
         ];
 
         ctrl.selectionChanged();
@@ -510,29 +484,23 @@ describe('ManageDashboards', () => {
         {
           id: 1,
           title: 'folder',
-          items: [
-            { id: 2, checked: true, slug: 'folder-dash' }
-          ],
+          items: [{ id: 2, checked: true, slug: 'folder-dash' }],
           checked: true,
-          slug: 'folder'
+          slug: 'folder',
         },
         {
           id: 3,
           title: 'folder-2',
-          items: [
-            { id: 3, checked: true, slug: 'folder-2-dash' }
-          ],
+          items: [{ id: 3, checked: true, slug: 'folder-2-dash' }],
           checked: false,
-          slug: 'folder-2'
+          slug: 'folder-2',
         },
         {
           id: 0,
           title: 'Root',
-          items: [
-            { id: 3, checked: true, slug: 'root-dash' }
-          ],
-          checked: true
-        }
+          items: [{ id: 3, checked: true, slug: 'root-dash' }],
+          checked: true,
+        },
       ];
 
       toBeDeleted = ctrl.getFoldersAndDashboardsToDelete();
@@ -567,20 +535,16 @@ describe('ManageDashboards', () => {
         {
           id: 1,
           title: 'folder',
-          items: [
-            { id: 2, checked: true, slug: 'dash' }
-          ],
+          items: [{ id: 2, checked: true, slug: 'dash' }],
           checked: false,
-          slug: 'folder'
+          slug: 'folder',
         },
         {
           id: 0,
           title: 'Root',
-          items: [
-            { id: 3, checked: true, slug: 'dash-2' }
-          ],
-          checked: false
-        }
+          items: [{ id: 3, checked: true, slug: 'dash-2' }],
+          checked: false,
+        },
       ];
     });
 
@@ -600,8 +564,12 @@ function createCtrlWithStubs(searchResponse: any, tags?: any) {
     },
     getDashboardTags: () => {
       return q.resolve(tags || []);
-    }
+    },
   };
 
-  return new ManageDashboardsCtrl({}, { getNav: () => { } }, <SearchSrv>searchSrvStub);
+  return new ManageDashboardsCtrl(
+    {},
+    { getNav: () => {} },
+    <SearchSrv>searchSrvStub
+  );
 }

+ 17 - 10
public/app/core/specs/org_switcher.jest.ts

@@ -1,10 +1,10 @@
-import {OrgSwitchCtrl} from '../components/org_switcher';
+import { OrgSwitchCtrl } from '../components/org_switcher';
 import q from 'q';
 
 jest.mock('app/core/services/context_srv', () => ({
   contextSrv: {
-    user: {orgId: 1}
-  }
+    user: { orgId: 1 },
+  },
 }));
 
 describe('OrgSwitcher', () => {
@@ -13,18 +13,23 @@ describe('OrgSwitcher', () => {
     let expectedUsingUrl;
 
     beforeEach(() => {
-
       const backendSrvStub: any = {
-        get: (url) => { return q.resolve([]); },
-        post: (url) => { expectedUsingUrl = url; return q.resolve({}); }
+        get: url => {
+          return q.resolve([]);
+        },
+        post: url => {
+          expectedUsingUrl = url;
+          return q.resolve({});
+        },
       };
 
       const orgSwitcherCtrl = new OrgSwitchCtrl(backendSrvStub);
 
-      orgSwitcherCtrl.getWindowLocationHref = () => 'http://localhost:3000?orgId=1&from=now-3h&to=now';
-      orgSwitcherCtrl.setWindowLocationHref = (href) => expectedHref = href;
+      orgSwitcherCtrl.getWindowLocationHref = () =>
+        'http://localhost:3000?orgId=1&from=now-3h&to=now';
+      orgSwitcherCtrl.setWindowLocationHref = href => (expectedHref = href);
 
-      return orgSwitcherCtrl.setUsingOrg({orgId: 2});
+      return orgSwitcherCtrl.setUsingOrg({ orgId: 2 });
     });
 
     it('should switch orgId in call to backend', () => {
@@ -32,7 +37,9 @@ describe('OrgSwitcher', () => {
     });
 
     it('should switch orgId in url', () => {
-      expect(expectedHref).toBe('http://localhost:3000?orgId=2&from=now-3h&to=now');
+      expect(expectedHref).toBe(
+        'http://localhost:3000?orgId=2&from=now-3h&to=now'
+      );
     });
   });
 });

+ 29 - 17
public/app/core/specs/rangeutil.jest.ts

@@ -2,17 +2,19 @@ import * as rangeUtil from 'app/core/utils/rangeutil';
 import _ from 'lodash';
 import moment from 'moment';
 
-describe("rangeUtil", () => {
-
-  describe("Can get range grouped list of ranges", () => {
+describe('rangeUtil', () => {
+  describe('Can get range grouped list of ranges', () => {
     it('when custom settings should return default range list', () => {
-      var groups = rangeUtil.getRelativeTimesList({time_options: []}, 'Last 5 minutes');
+      var groups = rangeUtil.getRelativeTimesList(
+        { time_options: [] },
+        'Last 5 minutes'
+      );
       expect(_.keys(groups).length).toBe(4);
       expect(groups[3][0].active).toBe(true);
     });
   });
 
-  describe("Can get range text described", () => {
+  describe('Can get range text described', () => {
     it('should handle simple old expression with only amount and unit', () => {
       var info = rangeUtil.describeTextRange('5m');
       expect(info.display).toBe('Last 5 minutes');
@@ -57,52 +59,62 @@ describe("rangeUtil", () => {
     });
   });
 
-  describe("Can get date range described", () => {
+  describe('Can get date range described', () => {
     it('Date range with simple ranges', () => {
-      var text = rangeUtil.describeTimeRange({from: 'now-1h', to: 'now'});
+      var text = rangeUtil.describeTimeRange({ from: 'now-1h', to: 'now' });
       expect(text).toBe('Last 1 hour');
     });
 
     it('Date range with rounding ranges', () => {
-      var text = rangeUtil.describeTimeRange({from: 'now/d+6h', to: 'now'});
+      var text = rangeUtil.describeTimeRange({ from: 'now/d+6h', to: 'now' });
       expect(text).toBe('now/d+6h to now');
     });
 
     it('Date range with absolute to now', () => {
-      var text = rangeUtil.describeTimeRange({from: moment([2014,10,10,2,3,4]), to: 'now'});
+      var text = rangeUtil.describeTimeRange({
+        from: moment([2014, 10, 10, 2, 3, 4]),
+        to: 'now',
+      });
       expect(text).toBe('Nov 10, 2014 02:03:04 to a few seconds ago');
     });
 
     it('Date range with absolute to relative', () => {
-      var text = rangeUtil.describeTimeRange({from: moment([2014,10,10,2,3,4]), to: 'now-1d'});
+      var text = rangeUtil.describeTimeRange({
+        from: moment([2014, 10, 10, 2, 3, 4]),
+        to: 'now-1d',
+      });
       expect(text).toBe('Nov 10, 2014 02:03:04 to a day ago');
     });
 
     it('Date range with relative to absolute', () => {
-      var text = rangeUtil.describeTimeRange({from: 'now-7d', to: moment([2014,10,10,2,3,4])});
+      var text = rangeUtil.describeTimeRange({
+        from: 'now-7d',
+        to: moment([2014, 10, 10, 2, 3, 4]),
+      });
       expect(text).toBe('7 days ago to Nov 10, 2014 02:03:04');
     });
 
     it('Date range with non matching default ranges', () => {
-      var text = rangeUtil.describeTimeRange({from: 'now-13h', to: 'now'});
+      var text = rangeUtil.describeTimeRange({ from: 'now-13h', to: 'now' });
       expect(text).toBe('Last 13 hours');
     });
 
     it('Date range with from and to both are in now-* format', () => {
-      var text = rangeUtil.describeTimeRange({from: 'now-6h', to: 'now-3h'});
+      var text = rangeUtil.describeTimeRange({ from: 'now-6h', to: 'now-3h' });
       expect(text).toBe('now-6h to now-3h');
     });
 
     it('Date range with from and to both are either in now-* or now/* format', () => {
-      var text = rangeUtil.describeTimeRange({from: 'now/d+6h', to: 'now-3h'});
+      var text = rangeUtil.describeTimeRange({
+        from: 'now/d+6h',
+        to: 'now-3h',
+      });
       expect(text).toBe('now/d+6h to now-3h');
     });
 
     it('Date range with from and to both are either in now-* or now+* format', () => {
-      var text = rangeUtil.describeTimeRange({from: 'now-6h', to: 'now+1h'});
+      var text = rangeUtil.describeTimeRange({ from: 'now-6h', to: 'now+1h' });
       expect(text).toBe('now-6h to now+1h');
     });
-
   });
-
 });

+ 19 - 26
public/app/core/specs/search.jest.ts

@@ -4,9 +4,14 @@ import { SearchSrv } from '../services/search_srv';
 describe('SearchCtrl', () => {
   const searchSrvStub = {
     search: (options: any) => {},
-    getDashboardTags: () => {}
+    getDashboardTags: () => {},
   };
-  let ctrl = new SearchCtrl({}, {}, {}, <SearchSrv>searchSrvStub, { onAppEvent: () => { } });
+  let ctrl = new SearchCtrl(
+    { $on: () => {} },
+    {},
+    {},
+    <SearchSrv>searchSrvStub
+  );
 
   describe('Given an empty result', () => {
     beforeEach(() => {
@@ -45,19 +50,16 @@ describe('SearchCtrl', () => {
           items: [],
           selected: true,
           expanded: false,
-          toggle: (i) => i.expanded = !i.expanded
+          toggle: i => (i.expanded = !i.expanded),
         },
         {
           id: 0,
           title: 'Root',
-          items: [
-            { id: 3, selected: false },
-            { id: 5, selected: false }
-          ],
+          items: [{ id: 3, selected: false }, { id: 5, selected: false }],
           selected: false,
           expanded: true,
-          toggle: (i) => i.expanded = !i.expanded
-        }
+          toggle: i => (i.expanded = !i.expanded),
+        },
       ];
     });
 
@@ -142,25 +144,19 @@ describe('SearchCtrl', () => {
         {
           id: 1,
           title: 'folder',
-          items: [
-            { id: 2, selected: false },
-            { id: 4, selected: false }
-          ],
+          items: [{ id: 2, selected: false }, { id: 4, selected: false }],
           selected: true,
           expanded: false,
-          toggle: (i) => i.expanded = !i.expanded
+          toggle: i => (i.expanded = !i.expanded),
         },
         {
           id: 0,
           title: 'Root',
-          items: [
-            { id: 3, selected: false },
-            { id: 5, selected: false }
-          ],
+          items: [{ id: 3, selected: false }, { id: 5, selected: false }],
           selected: false,
           expanded: true,
-          toggle: (i) => i.expanded = !i.expanded
-        }
+          toggle: i => (i.expanded = !i.expanded),
+        },
       ];
     });
 
@@ -252,14 +248,11 @@ describe('SearchCtrl', () => {
       ctrl.results = [
         {
           hideHeader: true,
-          items: [
-            { id: 3, selected: true },
-            { id: 5, selected: false }
-          ],
+          items: [{ id: 3, selected: true }, { id: 5, selected: false }],
           selected: false,
           expanded: true,
-          toggle: (i) => i.expanded = !i.expanded
-        }
+          toggle: i => (i.expanded = !i.expanded),
+        },
       ];
     });
 

+ 57 - 10
public/app/core/specs/search_results.jest.ts

@@ -1,15 +1,23 @@
 import { SearchResultsCtrl } from '../components/search/search_results';
+import { beforeEach, afterEach } from 'test/lib/common';
+import appEvents from 'app/core/app_events';
+
+jest.mock('app/core/app_events', () => {
+  return {
+    emit: jest.fn<any>(),
+  };
+});
 
 describe('SearchResultsCtrl', () => {
   let ctrl;
 
   describe('when checking an item that is not checked', () => {
-    let item = {checked: false};
+    let item = { checked: false };
     let selectionChanged = false;
 
     beforeEach(() => {
       ctrl = new SearchResultsCtrl({});
-      ctrl.onSelectionChanged = () => selectionChanged = true;
+      ctrl.onSelectionChanged = () => (selectionChanged = true);
       ctrl.toggleSelection(item);
     });
 
@@ -23,12 +31,12 @@ describe('SearchResultsCtrl', () => {
   });
 
   describe('when checking an item that is checked', () => {
-    let item = {checked: true};
+    let item = { checked: true };
     let selectionChanged = false;
 
     beforeEach(() => {
       ctrl = new SearchResultsCtrl({});
-      ctrl.onSelectionChanged = () => selectionChanged = true;
+      ctrl.onSelectionChanged = () => (selectionChanged = true);
       ctrl.toggleSelection(item);
     });
 
@@ -46,12 +54,12 @@ describe('SearchResultsCtrl', () => {
 
     beforeEach(() => {
       ctrl = new SearchResultsCtrl({});
-      ctrl.onTagSelected = (tag) => selectedTag = tag;
+      ctrl.onTagSelected = tag => (selectedTag = tag);
       ctrl.selectTag('tag-test');
     });
 
     it('should trigger tag selected callback', () => {
-      expect(selectedTag["$tag"]).toBe('tag-test');
+      expect(selectedTag['$tag']).toBe('tag-test');
     });
   });
 
@@ -60,11 +68,13 @@ describe('SearchResultsCtrl', () => {
 
     beforeEach(() => {
       ctrl = new SearchResultsCtrl({});
-      ctrl.onFolderExpanding = () => { folderExpanded = true; };
+      ctrl.onFolderExpanding = () => {
+        folderExpanded = true;
+      };
 
       let folder = {
         expanded: false,
-        toggle: () => Promise.resolve(folder)
+        toggle: () => Promise.resolve(folder),
       };
 
       ctrl.toggleFolderExpand(folder);
@@ -80,11 +90,13 @@ describe('SearchResultsCtrl', () => {
 
     beforeEach(() => {
       ctrl = new SearchResultsCtrl({});
-      ctrl.onFolderExpanding = () => { folderExpanded = true; };
+      ctrl.onFolderExpanding = () => {
+        folderExpanded = true;
+      };
 
       let folder = {
         expanded: true,
-        toggle: () => Promise.resolve(folder)
+        toggle: () => Promise.resolve(folder),
       };
 
       ctrl.toggleFolderExpand(folder);
@@ -94,4 +106,39 @@ describe('SearchResultsCtrl', () => {
       expect(folderExpanded).toBeFalsy();
     });
   });
+
+  describe('when clicking on a link in search result', () => {
+    const dashPath = 'dashboard/path';
+    const $location = { path: () => dashPath };
+    const appEventsMock = appEvents as any;
+
+    describe('with the same url as current path', () => {
+      beforeEach(() => {
+        ctrl = new SearchResultsCtrl($location);
+        const item = { url: dashPath };
+        ctrl.onItemClick(item);
+      });
+
+      it('should close the search', () => {
+        expect(appEventsMock.emit.mock.calls.length).toBe(1);
+        expect(appEventsMock.emit.mock.calls[0][0]).toBe('hide-dash-search');
+      });
+    });
+
+    describe('with a different url than current path', () => {
+      beforeEach(() => {
+        ctrl = new SearchResultsCtrl($location);
+        const item = { url: 'another/path' };
+        ctrl.onItemClick(item);
+      });
+
+      it('should do nothing', () => {
+        expect(appEventsMock.emit.mock.calls.length).toBe(0);
+      });
+    });
+
+    afterEach(() => {
+      appEventsMock.emit.mockClear();
+    });
+  });
 });

+ 26 - 22
public/app/core/specs/search_srv.jest.ts

@@ -35,7 +35,10 @@ describe('SearchSrv', () => {
       backendSrvMock.search = jest
         .fn()
         .mockReturnValueOnce(
-          Promise.resolve([{ id: 2, title: 'second but first' }, { id: 1, title: 'first but second' }]),
+          Promise.resolve([
+            { id: 2, title: 'second but first' },
+            { id: 1, title: 'first but second' },
+          ])
         )
         .mockReturnValue(Promise.resolve([]));
 
@@ -47,7 +50,7 @@ describe('SearchSrv', () => {
     });
 
     it('should include recent dashboards section', () => {
-      expect(results[0].title).toBe('Recent Boards');
+      expect(results[0].title).toBe('Recent');
     });
 
     it('should return order decided by impressions store not api', () => {
@@ -62,11 +65,13 @@ describe('SearchSrv', () => {
         backendSrvMock.search = jest
           .fn()
           .mockReturnValueOnce(
-            Promise.resolve([{ id: 2, title: 'two' }, { id: 1, title: 'one' }]),
+            Promise.resolve([{ id: 2, title: 'two' }, { id: 1, title: 'one' }])
           )
           .mockReturnValue(Promise.resolve([]));
 
-        impressionSrv.getDashboardOpened = jest.fn().mockReturnValue([4, 5, 1, 2, 3]);
+        impressionSrv.getDashboardOpened = jest
+          .fn()
+          .mockReturnValue([4, 5, 1, 2, 3]);
 
         return searchSrv.search({ query: '' }).then(res => {
           results = res;
@@ -87,9 +92,7 @@ describe('SearchSrv', () => {
     beforeEach(() => {
       backendSrvMock.search = jest
         .fn()
-        .mockReturnValue(Promise.resolve([
-          {id: 1, title: 'starred'}
-        ]));
+        .mockReturnValue(Promise.resolve([{ id: 1, title: 'starred' }]));
 
       return searchSrv.search({ query: '' }).then(res => {
         results = res;
@@ -97,7 +100,7 @@ describe('SearchSrv', () => {
     });
 
     it('should include starred dashboards section', () => {
-      expect(results[0].title).toBe('Starred Boards');
+      expect(results[0].title).toBe('Starred');
       expect(results[0].items.length).toBe(1);
     });
   });
@@ -108,30 +111,31 @@ describe('SearchSrv', () => {
     beforeEach(() => {
       backendSrvMock.search = jest
         .fn()
-        .mockReturnValueOnce(Promise.resolve([
-          {id: 1, title: 'starred and recent', isStarred: true},
-          {id: 2, title: 'recent'}
-        ]))
-        .mockReturnValue(Promise.resolve([
-          {id: 1, title: 'starred and recent'}
-        ]));
-
-      impressionSrv.getDashboardOpened = jest.fn().mockReturnValue([1,2]);
+        .mockReturnValueOnce(
+          Promise.resolve([
+            { id: 1, title: 'starred and recent', isStarred: true },
+            { id: 2, title: 'recent' },
+          ])
+        )
+        .mockReturnValue(
+          Promise.resolve([{ id: 1, title: 'starred and recent' }])
+        );
+
+      impressionSrv.getDashboardOpened = jest.fn().mockReturnValue([1, 2]);
       return searchSrv.search({ query: '' }).then(res => {
         results = res;
       });
     });
 
     it('should not show starred in recent', () => {
-      expect(results[1].title).toBe('Recent Boards');
+      expect(results[1].title).toBe('Recent');
       expect(results[1].items[0].title).toBe('recent');
     });
 
     it('should show starred', () => {
-      expect(results[0].title).toBe('Starred Boards');
+      expect(results[0].title).toBe('Starred');
       expect(results[0].items[0].title).toBe('starred and recent');
     });
-
   });
 
   describe('with no query string and dashboards with folders returned', () => {
@@ -165,7 +169,7 @@ describe('SearchSrv', () => {
               id: 4,
               folderId: 1,
             },
-          ]),
+          ])
         );
 
       return searchSrv.search({ query: '' }).then(res => {
@@ -202,7 +206,7 @@ describe('SearchSrv', () => {
             folderId: 1,
             folderTitle: 'folder1',
           },
-        ]),
+        ])
       );
 
       return searchSrv.search({ query: 'search' }).then(res => {

Alguns ficheiros não foram mostrados porque muitos ficheiros mudaram neste diff