Просмотр исходного кода

Merge branch 'master' into walmartlabs-master

Torkel Ödegaard 8 лет назад
Родитель
Сommit
f3980504e2
100 измененных файлов с 1275 добавлено и 796 удалено
  1. 6 1
      CHANGELOG.md
  2. 15 5
      docs/sources/alerting/rules.md
  3. 2 1
      docs/sources/features/datasources/elasticsearch.md
  4. 1 1
      docs/sources/reference/dashboard.md
  5. 1 0
      pkg/api/api.go
  6. 2 0
      pkg/api/cloudwatch/metrics.go
  7. 8 1
      pkg/api/login_oauth.go
  8. 27 0
      pkg/api/metrics.go
  9. 2 2
      pkg/services/sqlstore/migrations/alert_mig.go
  10. 5 5
      pkg/services/sqlstore/migrations/dashboard_mig.go
  11. 4 4
      pkg/services/sqlstore/migrations/temp_user.go
  12. 26 0
      pkg/social/common.go
  13. 19 34
      pkg/social/generic_oauth.go
  14. 20 28
      pkg/social/github_oauth.go
  15. 8 6
      pkg/social/google_oauth.go
  16. 6 7
      pkg/social/grafana_com_oauth.go
  17. 16 3
      public/app/core/components/grafana_app.ts
  18. 2 2
      public/app/core/components/help/help.html
  19. 30 5
      public/app/core/components/navbar/navbar.html
  20. 21 6
      public/app/core/components/navbar/navbar.ts
  21. 56 42
      public/app/core/components/search/search.html
  22. 5 0
      public/app/core/components/search/search.ts
  23. 3 1
      public/app/core/controllers/error_ctrl.js
  24. 3 0
      public/app/core/core.ts
  25. 53 32
      public/app/core/directives/dash_edit_link.js
  26. 226 0
      public/app/core/nav_model_srv.ts
  27. 4 0
      public/app/core/routes/routes.ts
  28. 1 5
      public/app/core/services/keybindingSrv.ts
  29. 3 1
      public/app/core/time_series2.ts
  30. 10 7
      public/app/core/utils/file_export.ts
  31. 11 3
      public/app/features/admin/admin.ts
  32. 3 1
      public/app/features/admin/adminEditOrgCtrl.js
  33. 2 1
      public/app/features/admin/adminEditUserCtrl.js
  34. 2 1
      public/app/features/admin/adminListOrgsCtrl.js
  35. 3 1
      public/app/features/admin/admin_list_users_ctrl.ts
  36. 1 2
      public/app/features/admin/partials/admin_home.html
  37. 1 6
      public/app/features/admin/partials/edit_org.html
  38. 1 6
      public/app/features/admin/partials/edit_user.html
  39. 1 6
      public/app/features/admin/partials/new_user.html
  40. 1 6
      public/app/features/admin/partials/orgs.html
  41. 1 2
      public/app/features/admin/partials/settings.html
  42. 1 2
      public/app/features/admin/partials/stats.html
  43. 6 9
      public/app/features/admin/partials/users.html
  44. 6 5
      public/app/features/alerting/alert_list_ctrl.ts
  45. 4 1
      public/app/features/alerting/notification_edit_ctrl.ts
  46. 6 3
      public/app/features/alerting/notifications_list_ctrl.ts
  47. 1 2
      public/app/features/alerting/partials/alert_list.html
  48. 1 6
      public/app/features/alerting/partials/notification_edit.html
  49. 1 2
      public/app/features/alerting/partials/notifications_list.html
  50. 1 0
      public/app/features/dashboard/all.js
  51. 22 55
      public/app/features/dashboard/dashnav/dashnav.html
  52. 82 96
      public/app/features/dashboard/dashnav/dashnav.ts
  53. 32 0
      public/app/features/dashboard/export_data/export_data_modal.html
  54. 41 0
      public/app/features/dashboard/export_data/export_data_modal.ts
  55. 128 132
      public/app/features/dashboard/history/history.html
  56. 4 0
      public/app/features/dashboard/history/history.ts
  57. 0 1
      public/app/features/dashboard/history/partials/history.html
  58. 2 2
      public/app/features/dashboard/partials/saveDashboardMessage.html
  59. 1 2
      public/app/features/dashboard/timepicker/timepicker.ts
  60. 3 6
      public/app/features/dashboard/viewStateSrv.js
  61. 2 1
      public/app/features/org/change_password_ctrl.js
  62. 2 1
      public/app/features/org/newOrgCtrl.js
  63. 2 1
      public/app/features/org/orgApiKeysCtrl.js
  64. 2 1
      public/app/features/org/orgDetailsCtrl.js
  65. 3 1
      public/app/features/org/org_users_ctrl.ts
  66. 1 2
      public/app/features/org/partials/change_password.html
  67. 1 2
      public/app/features/org/partials/newOrg.html
  68. 1 2
      public/app/features/org/partials/orgApiKeys.html
  69. 1 2
      public/app/features/org/partials/orgDetails.html
  70. 1 2
      public/app/features/org/partials/orgUsers.html
  71. 2 3
      public/app/features/org/partials/profile.html
  72. 3 1
      public/app/features/org/profile_ctrl.ts
  73. 0 9
      public/app/features/panel/panel_ctrl.ts
  74. 1 14
      public/app/features/panel/panel_directive.ts
  75. 1 2
      public/app/features/playlist/partials/playlist.html
  76. 1 2
      public/app/features/playlist/partials/playlists.html
  77. 19 10
      public/app/features/playlist/playlist_edit_ctrl.ts
  78. 3 3
      public/app/features/playlist/playlist_srv.ts
  79. 8 6
      public/app/features/playlist/playlists_ctrl.ts
  80. 5 1
      public/app/features/playlist/specs/playlist_edit_ctrl_specs.ts
  81. 120 116
      public/app/features/plugins/ds_edit_ctrl.ts
  82. 26 16
      public/app/features/plugins/ds_list_ctrl.ts
  83. 1 2
      public/app/features/plugins/partials/ds_edit.html
  84. 1 5
      public/app/features/plugins/partials/ds_list.html
  85. 1 2
      public/app/features/plugins/partials/plugin_edit.html
  86. 1 2
      public/app/features/plugins/partials/plugin_list.html
  87. 1 2
      public/app/features/plugins/partials/plugin_page.html
  88. 12 7
      public/app/features/plugins/plugin_edit_ctrl.ts
  89. 3 1
      public/app/features/plugins/plugin_list_ctrl.ts
  90. 43 2
      public/app/features/plugins/plugin_page_ctrl.ts
  91. 1 2
      public/app/features/snapshot/partials/snapshots.html
  92. 10 1
      public/app/features/snapshot/snapshot_ctrl.ts
  93. 1 2
      public/app/features/styleguide/styleguide.html
  94. 3 1
      public/app/features/styleguide/styleguide.ts
  95. 1 0
      public/app/headers/common.d.ts
  96. 2 3
      public/app/partials/dashboard.html
  97. 1 2
      public/app/partials/error.html
  98. 6 2
      public/app/plugins/datasource/elasticsearch/query_builder.js
  99. 23 3
      public/app/plugins/datasource/grafana/datasource.ts
  100. 5 1
      public/app/plugins/datasource/grafana/partials/query.editor.html

+ 6 - 1
CHANGELOG.md

@@ -3,14 +3,19 @@
 ## Enhancements
 * **Elasticsearch**: Added filter aggregation label [#8420](https://github.com/grafana/grafana/pull/8420), thx [@tianzk](github.com/tianzk)
 * **Sensu**: Added option for source and handler [#8405](https://github.com/grafana/grafana/pull/8405), thx [@joemiller](github.com/joemiller)
+* **CSV**: Configurable csv export datetime format [#8058](https://github.com/grafana/grafana/issues/8058), thx [@cederigo](github.com/cederigo)
 
-# 4.3.2 (upcoming patch release)
+# 4.3.2 (2017-05-31)
 
 ## Bug fixes
 
 * **InfluxDB**: Fixed issue with query editor not showing ALIAS BY input field when in text editor mode [#8459](https://github.com/grafana/grafana/issues/8459)
 * **Graph Log Scale**: Fixed issue with log scale going below x-axis [#8244](https://github.com/grafana/grafana/issues/8244)
 * **Playlist**: Fixed dashboard play order issue [#7688](https://github.com/grafana/grafana/issues/7688)
+* **Elasticsearch**: Fixed table query issue with ES 2.x [#8467](https://github.com/grafana/grafana/issues/8467), thx [@goldeelox](https://github.com/goldeelox)
+
+## Changes
+* **Lazy Loading Of Panels**: Panels are no longer loaded as they are scrolled into view, this was reverted due to Chrome bug, might be reintroduced when Chrome fixes it's JS blocking behavior on scroll. [#8500](https://github.com/grafana/grafana/issues/8500)
 
 # 4.3.1 (2017-05-23)
 

+ 15 - 5
docs/sources/alerting/rules.md

@@ -52,12 +52,22 @@ Here you can specify the name of the alert rule and how often the scheduler shou
 ### Conditions
 
 Currently the only condition type that exists is a `Query` condition that allows you to
-specify a query letter, time range and an aggregation function. The letter refers to
-a query you already have added in the **Metrics** tab. The result from the query and the aggregation function is
-a single value that is then used in the threshold check. The query used in an alert rule cannot
-contain any template variables. Currently we only support `AND` and `OR` operators between conditions and they are executed serially.
+specify a query letter, time range and an aggregation function.
+
+
+### Query condition example
+
+```sql
+avg() OF query(A, 5m, now) IS BELOW 14
+```
+
+- `avg()` Controls how the values for **each** serie should be reduced to a value that can be compared against the threshold. Click on the function to change it to another aggregation function.
+- `query(A, 5m, now)`  The letter defines what query to execute from the **Metrics** tab. The second two parameters defines the time range, `5m, now` means 5 minutes from now to now. You can also do `10m, now-2m` to define a time range that will be 10 minutes from now to 2 minutes from now. This is useful if you want to ignore the last 2 minutes of data.
+- `IS BELOW 14`  Defines the type of threshold and the threshold value.  You can click on `IS BELOW` to change the type of threshold.
+
+The query used in an alert rule cannot contain any template variables. Currently we only support `AND` and `OR` operators between conditions and they are executed serially.
 For example, we have 3 conditions in the following order:
-`condition:A(evaluates to: TRUE) OR condition:B(evaluates to: FALSE) AND condition:C(evaluates to: TRUE)`
+*condition:A(evaluates to: TRUE) OR condition:B(evaluates to: FALSE) AND condition:C(evaluates to: TRUE)*
 so the result will be calculated as ((TRUE OR FALSE) AND TRUE) = TRUE.
 
 We plan to add other condition types in the future, like `Other Alert`, where you can include the state

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

@@ -92,9 +92,10 @@ The Elasticsearch data source supports two types of queries you can use in the *
 Query | Description
 ------------ | -------------
 *{"find": "fields", "type": "keyword"} | Returns a list of field names with the index type `keyword`.
-*{"find": "terms", "field": "@hostname"}* |  Returns a list of values for a field using term aggregation. Query will user current dashboard time range as time range for query.
+*{"find": "terms", "field": "@hostname", "size": 1000}* |  Returns a list of values for a field using term aggregation. Query will user current dashboard time range as time range for query.
 *{"find": "terms", "field": "@hostname", "query": '<lucene query>'}* | Returns a list of values for a field using term aggregation & and a specified lucene query filter. Query will use current dashboard time range as time range for query.
 
+There is a default size limit of 500 on terms queries. Set the size property in your query to set a custom limit.
 You can use other variables inside the query. Example query definition for a variable named `$host`.
 
 ```

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

@@ -65,7 +65,7 @@ Each field in the dashboard JSON is explained below with its usage:
 | **timezone** | timezone of dashboard, i.e. `utc` or `browser` |
 | **editable** | whether a dashboard is editable or not |
 | **hideControls** | whether row controls on the left in green are hidden or not |
-| **graphTooltip** | TODO |
+| **graphTooltip** | 0 for no shared crosshair or tooltip (default), 1 for shared crosshair, 2 for shared crosshair AND shared tooltip |
 | **rows** | row metadata, see [rows section](#rows) for details |
 | **time** | time range for dashboard, i.e. last 6 hours, last 7 days, etc |
 | **timepicker** | timepicker metadata, see [timepicker section](#timepicker) for details |

+ 1 - 0
pkg/api/api.go

@@ -261,6 +261,7 @@ func (hs *HttpServer) registerRoutes() {
 		r.Post("/tsdb/query", bind(dtos.MetricRequest{}), wrap(QueryMetrics))
 		r.Get("/tsdb/testdata/scenarios", wrap(GetTestDataScenarios))
 		r.Get("/tsdb/testdata/gensql", reqGrafanaAdmin, wrap(GenerateSqlTestData))
+		r.Get("/tsdb/testdata/random-walk", wrap(GetTestDataRandomWalk))
 
 		// metrics
 		r.Get("/metrics", wrap(GetInternalMetrics))

+ 2 - 0
pkg/api/cloudwatch/metrics.go

@@ -90,6 +90,7 @@ func init() {
 			"CacheHitPercent", "CachePercentUsed", "CachePercentDirty", "ReadBytes", "ReadTime", "WriteBytes", "WriteTime", "QueuedWrites"},
 		"AWS/SWF": {"DecisionTaskScheduleToStartTime", "DecisionTaskStartToCloseTime", "DecisionTasksCompleted", "StartedDecisionTasksTimedOutOnClose", "WorkflowStartToCloseTime", "WorkflowsCanceled", "WorkflowsCompleted", "WorkflowsContinuedAsNew", "WorkflowsFailed", "WorkflowsTerminated", "WorkflowsTimedOut",
 			"ActivityTaskScheduleToCloseTime", "ActivityTaskScheduleToStartTime", "ActivityTaskStartToCloseTime", "ActivityTasksCanceled", "ActivityTasksCompleted", "ActivityTasksFailed", "ScheduledActivityTasksTimedOutOnClose", "ScheduledActivityTasksTimedOutOnStart", "StartedActivityTasksTimedOutOnClose", "StartedActivityTasksTimedOutOnHeartbeat"},
+		"AWS/VPN":        {"TunnelState", "TunnelDataIn", "TunnelDataOut"},
 		"AWS/WAF":        {"AllowedRequests", "BlockedRequests", "CountedRequests"},
 		"AWS/WorkSpaces": {"Available", "Unhealthy", "ConnectionAttempt", "ConnectionSuccess", "ConnectionFailure", "SessionLaunchTime", "InSessionLatency", "SessionDisconnect"},
 		"KMS":            {"SecondsUntilKeyMaterialExpiration"},
@@ -131,6 +132,7 @@ func init() {
 		"AWS/SQS":              {"QueueName"},
 		"AWS/StorageGateway":   {"GatewayId", "GatewayName", "VolumeId"},
 		"AWS/SWF":              {"Domain", "WorkflowTypeName", "WorkflowTypeVersion", "ActivityTypeName", "ActivityTypeVersion"},
+		"AWS/VPN":              {"VpnId", "TunnelIpAddress"},
 		"AWS/WAF":              {"Rule", "WebACL"},
 		"AWS/WorkSpaces":       {"DirectoryId", "WorkspaceId"},
 		"KMS":                  {"KeyId"},

+ 8 - 1
pkg/api/login_oauth.go

@@ -28,6 +28,7 @@ var (
 	ErrEmailNotAllowed       = errors.New("Required email domain not fulfilled")
 	ErrSignUpNotAllowed      = errors.New("Signup is not allowed for this adapter")
 	ErrUsersQuotaReached     = errors.New("Users quota reached")
+	ErrNoEmail               = errors.New("Login provider didn't return an email address")
 )
 
 func GenStateString() string {
@@ -63,7 +64,7 @@ func OAuthLogin(ctx *middleware.Context) {
 		if setting.OAuthService.OAuthInfos[name].HostedDomain == "" {
 			ctx.Redirect(connect.AuthCodeURL(state, oauth2.AccessTypeOnline))
 		} else {
-			ctx.Redirect(connect.AuthCodeURL(state, oauth2.SetParam("hd", setting.OAuthService.OAuthInfos[name].HostedDomain), oauth2.AccessTypeOnline))
+			ctx.Redirect(connect.AuthCodeURL(state, oauth2.SetAuthURLParam("hd", setting.OAuthService.OAuthInfos[name].HostedDomain), oauth2.AccessTypeOnline))
 		}
 		return
 	}
@@ -134,6 +135,12 @@ func OAuthLogin(ctx *middleware.Context) {
 
 	ctx.Logger.Debug("OAuthLogin got user info", "userInfo", userInfo)
 
+	// validate that we got at least an email address
+	if userInfo.Email == "" {
+		redirectWithError(ctx, ErrNoEmail)
+		return
+	}
+
 	// validate that the email is allowed to login to grafana
 	if !connect.IsEmailAllowed(userInfo.Email) {
 		redirectWithError(ctx, ErrEmailNotAllowed)

+ 27 - 0
pkg/api/metrics.go

@@ -7,6 +7,7 @@ import (
 
 	"github.com/grafana/grafana/pkg/api/dtos"
 	"github.com/grafana/grafana/pkg/bus"
+	"github.com/grafana/grafana/pkg/components/simplejson"
 	"github.com/grafana/grafana/pkg/metrics"
 	"github.com/grafana/grafana/pkg/middleware"
 	"github.com/grafana/grafana/pkg/models"
@@ -144,3 +145,29 @@ func GenerateSqlTestData(c *middleware.Context) Response {
 
 	return Json(200, &util.DynMap{"message": "OK"})
 }
+
+// GET /api/tsdb/testdata/random-walk
+func GetTestDataRandomWalk(c *middleware.Context) Response {
+	from := c.Query("from")
+	to := c.Query("to")
+	intervalMs := c.QueryInt64("intervalMs")
+
+	timeRange := tsdb.NewTimeRange(from, to)
+	request := &tsdb.Request{TimeRange: timeRange}
+
+	request.Queries = append(request.Queries, &tsdb.Query{
+		RefId:      "A",
+		IntervalMs: intervalMs,
+		Model: simplejson.NewFromAny(&util.DynMap{
+			"scenario": "random_walk",
+		}),
+		DataSource: &models.DataSource{Type: "grafana-testdata-datasource"},
+	})
+
+	resp, err := tsdb.HandleRequest(context.Background(), request)
+	if err != nil {
+		return ApiError(500, "Metric request error", err)
+	}
+
+	return Json(200, &resp)
+}

+ 2 - 2
pkg/services/sqlstore/migrations/alert_mig.go

@@ -16,7 +16,7 @@ func addAlertMigrations(mg *Migrator) {
 			{Name: "org_id", Type: DB_BigInt, Nullable: false},
 			{Name: "name", Type: DB_NVarchar, Length: 255, Nullable: false},
 			{Name: "message", Type: DB_Text, Nullable: false},
-			{Name: "state", Type: DB_NVarchar, Length: 255, Nullable: false},
+			{Name: "state", Type: DB_NVarchar, Length: 190, Nullable: false},
 			{Name: "settings", Type: DB_Text, Nullable: false},
 			{Name: "frequency", Type: DB_BigInt, Nullable: false},
 			{Name: "handler", Type: DB_BigInt, Nullable: false},
@@ -70,7 +70,7 @@ func addAlertMigrations(mg *Migrator) {
 	mg.AddMigration("Update alert table charset", NewTableCharsetMigration("alert", []*Column{
 		{Name: "name", Type: DB_NVarchar, Length: 255, Nullable: false},
 		{Name: "message", Type: DB_Text, Nullable: false},
-		{Name: "state", Type: DB_NVarchar, Length: 255, Nullable: false},
+		{Name: "state", Type: DB_NVarchar, Length: 190, Nullable: false},
 		{Name: "settings", Type: DB_Text, Nullable: false},
 		{Name: "severity", Type: DB_Text, Nullable: false},
 		{Name: "execution_error", Type: DB_Text, Nullable: false},

+ 5 - 5
pkg/services/sqlstore/migrations/dashboard_mig.go

@@ -8,7 +8,7 @@ func addDashboardMigration(mg *Migrator) {
 		Columns: []*Column{
 			{Name: "id", Type: DB_BigInt, IsPrimaryKey: true, IsAutoIncrement: true},
 			{Name: "version", Type: DB_Int, Nullable: false},
-			{Name: "slug", Type: DB_NVarchar, Length: 190, Nullable: false},
+			{Name: "slug", Type: DB_NVarchar, Length: 189, Nullable: false},
 			{Name: "title", Type: DB_NVarchar, Length: 255, Nullable: false},
 			{Name: "data", Type: DB_Text, Nullable: false},
 			{Name: "account_id", Type: DB_BigInt, Nullable: false},
@@ -56,7 +56,7 @@ func addDashboardMigration(mg *Migrator) {
 		Columns: []*Column{
 			{Name: "id", Type: DB_BigInt, IsPrimaryKey: true, IsAutoIncrement: true},
 			{Name: "version", Type: DB_Int, Nullable: false},
-			{Name: "slug", Type: DB_NVarchar, Length: 190, Nullable: false},
+			{Name: "slug", Type: DB_NVarchar, Length: 189, Nullable: false},
 			{Name: "title", Type: DB_NVarchar, Length: 255, Nullable: false},
 			{Name: "data", Type: DB_Text, Nullable: false},
 			{Name: "org_id", Type: DB_BigInt, Nullable: false},
@@ -114,7 +114,7 @@ func addDashboardMigration(mg *Migrator) {
 
 	// add column to store plugin_id
 	mg.AddMigration("Add column plugin_id in dashboard", NewAddColumnMigration(dashboardV2, &Column{
-		Name: "plugin_id", Type: DB_NVarchar, Nullable: true, Length: 255,
+		Name: "plugin_id", Type: DB_NVarchar, Nullable: true, Length: 189,
 	}))
 
 	mg.AddMigration("Add index for plugin_id in dashboard", NewAddIndexMigration(dashboardV2, &Index{
@@ -127,9 +127,9 @@ func addDashboardMigration(mg *Migrator) {
 	}))
 
 	mg.AddMigration("Update dashboard table charset", NewTableCharsetMigration("dashboard", []*Column{
-		{Name: "slug", Type: DB_NVarchar, Length: 190, Nullable: false},
+		{Name: "slug", Type: DB_NVarchar, Length: 189, Nullable: false},
 		{Name: "title", Type: DB_NVarchar, Length: 255, Nullable: false},
-		{Name: "plugin_id", Type: DB_NVarchar, Nullable: true, Length: 255},
+		{Name: "plugin_id", Type: DB_NVarchar, Nullable: true, Length: 189},
 		{Name: "data", Type: DB_MediumText, Nullable: false},
 	}))
 

+ 4 - 4
pkg/services/sqlstore/migrations/temp_user.go

@@ -9,10 +9,10 @@ func addTempUserMigrations(mg *Migrator) {
 			{Name: "id", Type: DB_BigInt, IsPrimaryKey: true, IsAutoIncrement: true},
 			{Name: "org_id", Type: DB_BigInt, Nullable: false},
 			{Name: "version", Type: DB_Int, Nullable: false},
-			{Name: "email", Type: DB_NVarchar, Length: 255},
+			{Name: "email", Type: DB_NVarchar, Length: 190},
 			{Name: "name", Type: DB_NVarchar, Length: 255, Nullable: true},
 			{Name: "role", Type: DB_NVarchar, Length: 20, Nullable: true},
-			{Name: "code", Type: DB_NVarchar, Length: 255},
+			{Name: "code", Type: DB_NVarchar, Length: 190},
 			{Name: "status", Type: DB_Varchar, Length: 20},
 			{Name: "invited_by_user_id", Type: DB_BigInt, Nullable: true},
 			{Name: "email_sent", Type: DB_Bool},
@@ -37,10 +37,10 @@ func addTempUserMigrations(mg *Migrator) {
 	addTableIndicesMigrations(mg, "v1-7", tempUserV1)
 
 	mg.AddMigration("Update temp_user table charset", NewTableCharsetMigration("temp_user", []*Column{
-		{Name: "email", Type: DB_NVarchar, Length: 255},
+		{Name: "email", Type: DB_NVarchar, Length: 190},
 		{Name: "name", Type: DB_NVarchar, Length: 255, Nullable: true},
 		{Name: "role", Type: DB_NVarchar, Length: 20, Nullable: true},
-		{Name: "code", Type: DB_NVarchar, Length: 255},
+		{Name: "code", Type: DB_NVarchar, Length: 190},
 		{Name: "status", Type: DB_Varchar, Length: 20},
 		{Name: "remote_addr", Type: DB_Varchar, Length: 255, Nullable: true},
 	}))

+ 26 - 0
pkg/social/common.go

@@ -2,7 +2,11 @@ package social
 
 import (
 	"fmt"
+	"io/ioutil"
+	"net/http"
 	"strings"
+
+	"github.com/grafana/grafana/pkg/log"
 )
 
 func isEmailAllowed(email string, allowedDomains []string) bool {
@@ -18,3 +22,25 @@ func isEmailAllowed(email string, allowedDomains []string) bool {
 
 	return valid
 }
+
+func HttpGet(client *http.Client, url string) ([]byte, error) {
+	r, err := client.Get(url)
+	if err != nil {
+		return nil, err
+	}
+
+	defer r.Body.Close()
+
+	body, err := ioutil.ReadAll(r.Body)
+	if err != nil {
+		return nil, err
+	}
+
+	if r.StatusCode >= 300 {
+		return nil, fmt.Errorf(string(body))
+	}
+
+	log.Trace("HTTP GET %s: %s %s", url, r.Status, string(body))
+
+	return body, nil
+}

+ 19 - 34
pkg/social/generic_oauth.go

@@ -4,7 +4,6 @@ import (
 	"encoding/json"
 	"errors"
 	"fmt"
-	"io/ioutil"
 	"net/http"
 
 	"github.com/grafana/grafana/pkg/models"
@@ -84,22 +83,14 @@ func (s *GenericOAuth) FetchPrivateEmail(client *http.Client) (string, error) {
 		IsConfirmed bool   `json:"is_confirmed"`
 	}
 
-	emailsUrl := fmt.Sprintf(s.apiUrl + "/emails")
-	r, err := client.Get(emailsUrl)
+	body, err := HttpGet(client, fmt.Sprintf(s.apiUrl+"/emails"))
 	if err != nil {
-		return "", err
+		return "", fmt.Errorf("Error getting email address: %s", err)
 	}
 
-	defer r.Body.Close()
-
 	var records []Record
 
-	body, err := ioutil.ReadAll(r.Body)
-	if err != nil {
-		return "", err
-	}
-
-	err = json.Unmarshal(body, records)
+	err = json.Unmarshal(body, &records)
 	if err != nil {
 		var data struct {
 			Values []Record `json:"values"`
@@ -107,7 +98,7 @@ func (s *GenericOAuth) FetchPrivateEmail(client *http.Client) (string, error) {
 
 		err = json.Unmarshal(body, &data)
 		if err != nil {
-			return "", err
+			return "", fmt.Errorf("Error getting email address: %s", err)
 		}
 
 		records = data.Values
@@ -129,18 +120,16 @@ func (s *GenericOAuth) FetchTeamMemberships(client *http.Client) ([]int, error)
 		Id int `json:"id"`
 	}
 
-	membershipUrl := fmt.Sprintf(s.apiUrl + "/teams")
-	r, err := client.Get(membershipUrl)
+	body, err := HttpGet(client, fmt.Sprintf(s.apiUrl+"/teams"))
 	if err != nil {
-		return nil, err
+		return nil, fmt.Errorf("Error getting team memberships: %s", err)
 	}
 
-	defer r.Body.Close()
-
 	var records []Record
 
-	if err = json.NewDecoder(r.Body).Decode(&records); err != nil {
-		return nil, err
+	err = json.Unmarshal(body, &records)
+	if err != nil {
+		return nil, fmt.Errorf("Error getting team memberships: %s", err)
 	}
 
 	var ids = make([]int, len(records))
@@ -156,18 +145,16 @@ func (s *GenericOAuth) FetchOrganizations(client *http.Client) ([]string, error)
 		Login string `json:"login"`
 	}
 
-	url := fmt.Sprintf(s.apiUrl + "/orgs")
-	r, err := client.Get(url)
+	body, err := HttpGet(client, fmt.Sprintf(s.apiUrl+"/orgs"))
 	if err != nil {
-		return nil, err
+		return nil, fmt.Errorf("Error getting organizations: %s", err)
 	}
 
-	defer r.Body.Close()
-
 	var records []Record
 
-	if err = json.NewDecoder(r.Body).Decode(&records); err != nil {
-		return nil, err
+	err = json.Unmarshal(body, &records)
+	if err != nil {
+		return nil, fmt.Errorf("Error getting organizations: %s", err)
 	}
 
 	var logins = make([]string, len(records))
@@ -188,16 +175,14 @@ func (s *GenericOAuth) UserInfo(client *http.Client) (*BasicUserInfo, error) {
 		Attributes  map[string][]string `json:"attributes"`
 	}
 
-	var err error
-	r, err := client.Get(s.apiUrl)
+	body, err := HttpGet(client, s.apiUrl)
 	if err != nil {
-		return nil, err
+		return nil, fmt.Errorf("Error getting user info: %s", err)
 	}
 
-	defer r.Body.Close()
-
-	if err = json.NewDecoder(r.Body).Decode(&data); err != nil {
-		return nil, err
+	err = json.Unmarshal(body, &data)
+	if err != nil {
+		return nil, fmt.Errorf("Error getting user info: %s", err)
 	}
 
 	userInfo := &BasicUserInfo{

+ 20 - 28
pkg/social/github_oauth.go

@@ -85,18 +85,16 @@ func (s *SocialGithub) FetchPrivateEmail(client *http.Client) (string, error) {
 		Verified bool   `json:"verified"`
 	}
 
-	emailsUrl := fmt.Sprintf(s.apiUrl + "/emails")
-	r, err := client.Get(emailsUrl)
+	body, err := HttpGet(client, fmt.Sprintf(s.apiUrl+"/emails"))
 	if err != nil {
-		return "", err
+		return "", fmt.Errorf("Error getting email address: %s", err)
 	}
 
-	defer r.Body.Close()
-
 	var records []Record
 
-	if err = json.NewDecoder(r.Body).Decode(&records); err != nil {
-		return "", err
+	err = json.Unmarshal(body, &records)
+	if err != nil {
+		return "", fmt.Errorf("Error getting email address: %s", err)
 	}
 
 	var email = ""
@@ -114,18 +112,16 @@ func (s *SocialGithub) FetchTeamMemberships(client *http.Client) ([]int, error)
 		Id int `json:"id"`
 	}
 
-	membershipUrl := fmt.Sprintf(s.apiUrl + "/teams")
-	r, err := client.Get(membershipUrl)
+	body, err := HttpGet(client, fmt.Sprintf(s.apiUrl+"/teams"))
 	if err != nil {
-		return nil, err
+		return nil, fmt.Errorf("Error getting team memberships: %s", err)
 	}
 
-	defer r.Body.Close()
-
 	var records []Record
 
-	if err = json.NewDecoder(r.Body).Decode(&records); err != nil {
-		return nil, err
+	err = json.Unmarshal(body, &records)
+	if err != nil {
+		return nil, fmt.Errorf("Error getting team memberships: %s", err)
 	}
 
 	var ids = make([]int, len(records))
@@ -141,18 +137,16 @@ func (s *SocialGithub) FetchOrganizations(client *http.Client) ([]string, error)
 		Login string `json:"login"`
 	}
 
-	url := fmt.Sprintf(s.apiUrl + "/orgs")
-	r, err := client.Get(url)
+	body, err := HttpGet(client, fmt.Sprintf(s.apiUrl+"/orgs"))
 	if err != nil {
-		return nil, err
+		return nil, fmt.Errorf("Error getting organizations: %s", err)
 	}
 
-	defer r.Body.Close()
-
 	var records []Record
 
-	if err = json.NewDecoder(r.Body).Decode(&records); err != nil {
-		return nil, err
+	err = json.Unmarshal(body, &records)
+	if err != nil {
+		return nil, fmt.Errorf("Error getting organizations: %s", err)
 	}
 
 	var logins = make([]string, len(records))
@@ -170,16 +164,14 @@ func (s *SocialGithub) UserInfo(client *http.Client) (*BasicUserInfo, error) {
 		Email string `json:"email"`
 	}
 
-	var err error
-	r, err := client.Get(s.apiUrl)
+	body, err := HttpGet(client, s.apiUrl)
 	if err != nil {
-		return nil, err
+		return nil, fmt.Errorf("Error getting user info: %s", err)
 	}
 
-	defer r.Body.Close()
-
-	if err = json.NewDecoder(r.Body).Decode(&data); err != nil {
-		return nil, err
+	err = json.Unmarshal(body, &data)
+	if err != nil {
+		return nil, fmt.Errorf("Error getting user info: %s", err)
 	}
 
 	userInfo := &BasicUserInfo{

+ 8 - 6
pkg/social/google_oauth.go

@@ -2,6 +2,7 @@ package social
 
 import (
 	"encoding/json"
+	"fmt"
 	"net/http"
 
 	"github.com/grafana/grafana/pkg/models"
@@ -34,16 +35,17 @@ func (s *SocialGoogle) UserInfo(client *http.Client) (*BasicUserInfo, error) {
 		Name  string `json:"name"`
 		Email string `json:"email"`
 	}
-	var err error
 
-	r, err := client.Get(s.apiUrl)
+	body, err := HttpGet(client, s.apiUrl)
 	if err != nil {
-		return nil, err
+		return nil, fmt.Errorf("Error getting user info: %s", err)
 	}
-	defer r.Body.Close()
-	if err = json.NewDecoder(r.Body).Decode(&data); err != nil {
-		return nil, err
+
+	err = json.Unmarshal(body, &data)
+	if err != nil {
+		return nil, fmt.Errorf("Error getting user info: %s", err)
 	}
+
 	return &BasicUserInfo{
 		Name:  data.Name,
 		Email: data.Email,

+ 6 - 7
pkg/social/grafana_com_oauth.go

@@ -2,6 +2,7 @@ package social
 
 import (
 	"encoding/json"
+	"fmt"
 	"net/http"
 
 	"github.com/grafana/grafana/pkg/models"
@@ -57,16 +58,14 @@ func (s *SocialGrafanaCom) UserInfo(client *http.Client) (*BasicUserInfo, error)
 		Orgs  []OrgRecord `json:"orgs"`
 	}
 
-	var err error
-	r, err := client.Get(s.url + "/api/oauth2/user")
+	body, err := HttpGet(client, s.url+"/api/oauth2/user")
 	if err != nil {
-		return nil, err
+		return nil, fmt.Errorf("Error getting user info: %s", err)
 	}
 
-	defer r.Body.Close()
-
-	if err = json.NewDecoder(r.Body).Decode(&data); err != nil {
-		return nil, err
+	err = json.Unmarshal(body, &data)
+	if err != nil {
+		return nil, fmt.Errorf("Error getting user info: %s", err)
 	}
 
 	userInfo := &BasicUserInfo{

+ 16 - 3
public/app/core/components/grafana_app.ts

@@ -105,10 +105,14 @@ export function grafanaAppDirective(playlistSrv, contextSrv) {
         if (pageClass) {
           body.removeClass(pageClass);
         }
-        pageClass = data.$$route.pageClass;
-        if (pageClass) {
-          body.addClass(pageClass);
+
+        if (data.$$route) {
+          pageClass = data.$$route.pageClass;
+          if (pageClass) {
+            body.addClass(pageClass);
+          }
         }
+
         $("#tooltip, .tooltip").remove();
 
         // check for kiosk url param
@@ -194,6 +198,15 @@ export function grafanaAppDirective(playlistSrv, contextSrv) {
             });
           }
         }
+
+        // hide menus
+        var openMenus = body.find('.navbar-page-btn--open');
+        if (openMenus.length > 0) {
+          if (target.parents('.navbar-page-btn--open').length === 0) {
+            openMenus.removeClass('navbar-page-btn--open');
+          }
+        }
+
         // hide sidemenu
         if (!ignoreSideMenuHide && !contextSrv.pinned && body.find('.sidemenu').length > 0) {
           if (target.parents('.sidemenu').length === 0) {

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

@@ -1,7 +1,7 @@
 <div class="modal-body">
 	<div class="modal-header">
 		<h2 class="modal-header-title">
-			<i class="fa fa-keyboard"></i>
+			<i class="fa fa-keyboard-o"></i>
 			<span class="p-l-1">Shortcuts</span>
 		</h2>
 
@@ -20,7 +20,7 @@
 
 	<div class="modal-content help-modal">
 
-		<p class="small" style="position: absolute; top: 48px; right: 10px">
+		<p class="small" style="position: absolute; top: 13px; right: 44px">
 			<span class="shortcut-table-key">mod</span> =
 			<span class="muted">CTRL on windows or linux and CMD key on Mac</span>
 		</p>

+ 30 - 5
public/app/core/components/navbar/navbar.html

@@ -8,11 +8,36 @@
 		<i class="fa fa-chevron-left"></i>
 	</a>
 
-	<a href="{{ctrl.titleUrl}}" class="navbar-page-btn" ng-show="ctrl.title">
-		<i class="{{ctrl.icon}}" ng-show="ctrl.icon"></i>
-		<img ng-src="{{ctrl.iconUrl}}" ng-show="ctrl.iconUrl"></i>
-		{{ctrl.title}}
+  <a class="navbar-page-btn" ng-click="ctrl.showSearch()">
+		<i class="fa fa-search"></i>
 	</a>
 
-	<div ng-transclude></div>
+	<div ng-if="::!ctrl.hasMenu">
+		<a href="{{::ctrl.section.url}}" class="navbar-page-btn">
+      <i class="{{::ctrl.section.icon}}" ng-show="::ctrl.section.icon"></i>
+      <img ng-src="{{::ctrl.section.iconUrl}}" ng-show="::ctrl.section.iconUrl"></i>
+      {{::ctrl.section.title}}
+    </a>
+	</div>
+
+  <div class="dropdown navbar-section-wrapper"  ng-if="::ctrl.hasMenu">
+    <a href="{{::ctrl.section.url}}" class="navbar-page-btn" data-toggle="dropdown">
+      <i class="{{::ctrl.section.icon}}" ng-show="::ctrl.section.icon"></i>
+      <img ng-src="{{::ctrl.section.iconUrl}}" ng-show="::ctrl.section.iconUrl"></i>
+      {{::ctrl.section.title}}
+      <i class="fa fa-caret-down"></i>
+    </a>
+    <ul class="dropdown-menu dropdown-menu--navbar">
+      <li ng-repeat="navItem in ::ctrl.model.menu" ng-class="{active: navItem.active}">
+        <a class="pointer" ng-href="{{::navItem.url}}" ng-click="ctrl.navItemClicked(navItem, $event)">
+          <i class="{{::navItem.icon}}" ng-show="::navItem.icon"></i>
+          {{::navItem.title}}
+        </a>
+      </li>
+    </ul>
+  </div>
+
+  <div ng-transclude></div>
 </div>
+
+<dashboard-search></dashboard-search>

+ 21 - 6
public/app/core/components/navbar/navbar.ts

@@ -4,10 +4,28 @@ import config from 'app/core/config';
 import _ from 'lodash';
 import $ from 'jquery';
 import coreModule from '../../core_module';
+import {NavModel, NavModelItem}  from '../../nav_model_srv';
 
 export class NavbarCtrl {
+  model: NavModel;
+  section: NavModelItem;
+  hasMenu: boolean;
+
   /** @ngInject */
-  constructor(private $scope, private contextSrv) {
+  constructor(private $scope, private $rootScope, private contextSrv) {
+    this.section = this.model.section;
+    this.hasMenu = this.model.menu.length > 0;
+  }
+
+  showSearch() {
+    this.$rootScope.appEvent('show-dash-search');
+  }
+
+  navItemClicked(navItem, evt) {
+    if (navItem.clickHandler) {
+      navItem.clickHandler();
+      evt.preventDefault();
+    }
   }
 }
 
@@ -20,12 +38,9 @@ export function navbarDirective() {
     transclude: true,
     controllerAs: 'ctrl',
     scope: {
-      title: "@",
-      titleUrl: "@",
-      iconUrl: "@",
+      model: "=",
     },
-    link: function(scope, elem, attrs, ctrl) {
-      ctrl.icon = attrs.icon;
+    link: function(scope, elem) {
       elem.addClass('navbar');
     }
   };

+ 56 - 42
public/app/core/components/search/search.html

@@ -1,9 +1,22 @@
+
+<div class="search-backdrop" ng-if="ctrl.isOpen"></div>
+
 <div class="search-container" ng-if="ctrl.isOpen">
+
 	<div class="search-field-wrapper">
-		<span style="position: relative;">
-			<input  type="text" placeholder="Find dashboards by name" give-focus="ctrl.giveSearchFocus" tabindex="1"
-			ng-keydown="ctrl.keyDown($event)" ng-model="ctrl.query.query" ng-model-options="{ debounce: 500 }" spellcheck='false' ng-change="ctrl.search()" />
-		</span>
+		<div class="search-field-icon pointer" ng-click="ctrl.closeSearch()">
+			<i class="fa fa-search"></i>
+		</div>
+
+		<input type="text" placeholder="Find dashboards by name" give-focus="ctrl.giveSearchFocus" tabindex="1"
+						ng-keydown="ctrl.keyDown($event)"
+						ng-model="ctrl.query.query"
+						ng-model-options="{ debounce: 500 }"
+						spellcheck='false'
+						ng-change="ctrl.search()"
+						ng-blur="ctrl.searchInputBlur()"
+						/>
+
 		<div class="search-switches">
 			<i class="fa fa-filter"></i>
 			<a class="pointer" href="javascript:void 0;" ng-click="ctrl.showStarred()" tabindex="2">
@@ -24,54 +37,55 @@
 				</span>
 			</span>
 		</div>
+
+		<div class="search-field-spacer"></div>
 	</div>
 
-	<div class="search-results-container" ng-if="ctrl.tagsMode">
-		<div ng-repeat="tag in ctrl.results" class="pointer" style="width: 180px; float: left;"
-			ng-class="{'selected': $index === ctrl.selectedIndex }"
-			ng-click="ctrl.filterByTag(tag.term, $event)">
-			<a class="search-result-tag label label-tag" tag-color-from-name="tag.term">
-				<i class="fa fa-tag"></i>
-				<span>{{tag.term}} &nbsp;({{tag.count}})</span>
-			</a>
+	<div class="search-dropdown" ng-class="{'search-dropdown--fade-in': ctrl.openCompleted}">
+		<div class="search-results-container" ng-if="ctrl.tagsMode">
+			<div ng-repeat="tag in ctrl.results" class="pointer" style="width: 180px; float: left;"
+				ng-class="{'selected': $index === ctrl.selectedIndex }"
+				ng-click="ctrl.filterByTag(tag.term, $event)">
+				<a class="search-result-tag label label-tag" tag-color-from-name="tag.term">
+					<i class="fa fa-tag"></i>
+					<span>{{tag.term}} &nbsp;({{tag.count}})</span>
+				</a>
+			</div>
 		</div>
-	</div>
 
-	<div class="search-results-container" ng-if="!ctrl.tagsMode">
-		<h6 ng-hide="ctrl.results.length">No dashboards matching your query were found.</h6>
+		<div class="search-results-container" ng-if="!ctrl.tagsMode">
+			<h6 ng-hide="ctrl.results.length">No dashboards matching your query were found.</h6>
 
-		<a class="search-item pointer search-item-{{row.type}}" bindonce ng-repeat="row in ctrl.results"
-			ng-class="{'selected': $index == ctrl.selectedIndex}" ng-href="{{row.url}}">
+			<a class="search-item pointer search-item-{{row.type}}" bindonce ng-repeat="row in ctrl.results"
+				ng-class="{'selected': $index == ctrl.selectedIndex}" ng-href="{{row.url}}">
 
-			<span class="search-result-tags">
-				<span ng-click="ctrl.filterByTag(tag, $event)" ng-repeat="tag in row.tags" tag-color-from-name="tag"  class="label label-tag">
-					{{tag}}
+				<span class="search-result-tags">
+					<span ng-click="ctrl.filterByTag(tag, $event)" ng-repeat="tag in row.tags" tag-color-from-name="tag"  class="label label-tag">
+						{{tag}}
+					</span>
+					<i class="fa" ng-class="{'fa-star': row.isStarred, 'fa-star-o': !row.isStarred}"></i>
 				</span>
-				<i class="fa" ng-class="{'fa-star': row.isStarred, 'fa-star-o': !row.isStarred}"></i>
-			</span>
 
-			<span class="search-result-link">
-				<i class="fa search-result-icon"></i>
-				<span bo-text="row.title"></span>
-			</span>
-		</a>
-	</div>
-
-	<div class="search-button-row">
-		<a class="btn btn-inverse pull-left" href="dashboard/new" ng-show="ctrl.contextSrv.isEditor" ng-click="ctrl.isOpen = false;">
-			<i class="fa fa-plus"></i>
-			Create New
-		</a>
+				<span class="search-result-link">
+					<i class="fa search-result-icon"></i>
+					<span bo-text="row.title"></span>
+				</span>
+			</a>
+		</div>
 
-		<a class="btn btn-inverse pull-left" href="dashboard/new/?editview=import" ng-show="ctrl.contextSrv.isEditor" ng-click="ctrl.isOpen = false;">
-			<i class="fa fa-upload"></i>
-			Import
-		</a>
+		<div class="search-button-row">
+			<a class="btn btn-secondary" href="dashboard/new" ng-show="ctrl.contextSrv.isEditor" ng-click="ctrl.isOpen = false;">
+				<i class="fa fa-plus"></i>&nbsp; New Dashboard
+			</a>
 
-		<a class="pull-right search-button-row-explore-link" target="_blank" href="https://grafana.com/dashboards?utm_source=grafana_search">
-      		Find <img src="public/img/icn-dashboard-tiny.svg" width="14" /> dashboards on Grafana.com
-		</a>
+			<a class="btn btn-inverse" href="dashboard/new/?editview=import" ng-show="ctrl.contextSrv.isEditor" ng-click="ctrl.isOpen = false;">
+				<i class="fa fa-upload"></i>&nbsp; Import Dashboard
+			</a>
 
- 		<div class="clearfix"></div>
+			<a class="search-button-row-explore-link" target="_blank" href="https://grafana.com/dashboards?utm_source=grafana_search">
+				Find <img src="public/img/icn-dashboard-tiny.svg" width="14" /> dashboards on Grafana.com
+			</a>
+		</div>
 	</div>
 </div>
+

+ 5 - 0
public/app/core/components/search/search.ts

@@ -18,6 +18,8 @@ export class SearchCtrl {
   showImport: boolean;
   dismiss: any;
   ignoreClose: any;
+  // triggers fade animation class
+  openCompleted: boolean;
 
   /** @ngInject */
   constructor(private $scope, private $location, private $timeout, private backendSrv, private contextSrv, private $rootScope) {
@@ -27,6 +29,7 @@ export class SearchCtrl {
 
   closeSearch() {
     this.isOpen = this.ignoreClose;
+    this.openCompleted = false;
   }
 
   openSearch(evt, payload) {
@@ -56,6 +59,7 @@ export class SearchCtrl {
     }
 
     this.$timeout(() => {
+      this.openCompleted = true;
       this.ignoreClose = false;
       this.giveSearchFocus = this.giveSearchFocus + 1;
       this.search();
@@ -169,6 +173,7 @@ export function searchDirective() {
     controller: SearchCtrl,
     bindToController: true,
     controllerAs: 'ctrl',
+    scope: {},
   };
 }
 

+ 3 - 1
public/app/core/controllers/error_ctrl.js

@@ -5,7 +5,9 @@ define([
 function (angular, coreModule) {
   'use strict';
 
-  coreModule.default.controller('ErrorCtrl', function($scope, contextSrv) {
+  coreModule.default.controller('ErrorCtrl', function($scope, contextSrv, navModelSrv) {
+
+    $scope.navModel = navModelSrv.getNotFoundNav();
 
     var showSideMenu = contextSrv.sidemenu;
     contextSrv.sidemenu = false;

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

@@ -45,6 +45,7 @@ import {assignModelProperties} from './utils/model_utils';
 import {contextSrv} from './services/context_srv';
 import {KeybindingSrv} from './services/keybindingSrv';
 import {helpModal} from './components/help/help';
+import {NavModelSrv, NavModel} from './nav_model_srv';
 
 
 export {
@@ -69,4 +70,6 @@ export {
   contextSrv,
   KeybindingSrv,
   helpModal,
+  NavModelSrv,
+  NavModel,
 };

+ 53 - 32
public/app/core/directives/dash_edit_link.js

@@ -1,16 +1,18 @@
 define([
   'jquery',
+  'angular',
   '../core_module',
 ],
-function ($, coreModule) {
+function ($, angular, coreModule) {
   'use strict';
 
   var editViewMap = {
     'settings':    { src: 'public/app/features/dashboard/partials/settings.html'},
     'annotations': { src: 'public/app/features/annotations/partials/editor.html'},
-    'history':     { src: 'public/app/features/dashboard/history/partials/history.html'},
     'templating':  { src: 'public/app/features/templating/partials/editor.html'},
-    'import':      { src: '<dash-import></dash-import>' }
+    'history':     { html: '<gf-dashboard-history dashboard="dashboard"></gf-dashboard-history>'},
+    'timepicker':  { src: 'public/app/features/dashboard/timepicker/dropdown.html' },
+    'import':      { html: '<dash-import></dash-import>' }
   };
 
   coreModule.default.directive('dashEditorView', function($compile, $location, $rootScope) {
@@ -18,47 +20,53 @@ function ($, coreModule) {
       restrict: 'A',
       link: function(scope, elem) {
         var editorScope;
-        var lastEditor;
+        var lastEditView;
 
-        function hideEditorPane() {
+        function hideEditorPane(hideToShowOtherView) {
           if (editorScope) {
-            scope.appEvent('dash-editor-hidden', lastEditor);
-            editorScope.dismiss();
+            editorScope.dismiss(hideToShowOtherView);
+            scope.appEvent('dash-editor-hidden');
           }
         }
 
-        function showEditorPane(evt, payload, editview) {
-          if (editview) {
-            scope.contextSrv.editview = editViewMap[editview];
-            payload.src = scope.contextSrv.editview.src;
+        function showEditorPane(evt, options) {
+          if (options.editview) {
+            options.src = editViewMap[options.editview].src;
+            options.html = editViewMap[options.editview].html;
           }
 
-          if (lastEditor === payload.src) {
-            hideEditorPane();
+          if (lastEditView === options.editview) {
+            hideEditorPane(false);
             return;
           }
 
-          hideEditorPane();
+          hideEditorPane(true);
 
-          lastEditor = payload.src;
-          editorScope = payload.scope ? payload.scope.$new() : scope.$new();
+          lastEditView = options.editview;
+          editorScope = options.scope ? options.scope.$new() : scope.$new();
 
-          editorScope.dismiss = function() {
+          editorScope.dismiss = function(hideToShowOtherView) {
             editorScope.$destroy();
-            elem.empty();
-            lastEditor = null;
+            lastEditView = null;
             editorScope = null;
+            elem.removeClass('dash-edit-view--open');
+
+            if (!hideToShowOtherView) {
+              setTimeout(function() {
+                elem.empty();
+              }, 250);
+            }
 
-            if (editview) {
+            if (options.editview) {
               var urlParams = $location.search();
-              if (editview === urlParams.editview) {
+              if (options.editview === urlParams.editview) {
                 delete urlParams.editview;
                 $location.search(urlParams);
               }
             }
           };
 
-          if (editview === 'import') {
+          if (options.editview === 'import') {
             var modalScope = $rootScope.$new();
             modalScope.$on("$destroy", function() {
               editorScope.dismiss();
@@ -73,29 +81,42 @@ function ($, coreModule) {
             return;
           }
 
-          var view = payload.src;
-          if (view.indexOf('.html') > 0)  {
-            view = $('<div class="tabbed-view" ng-include="' + "'" + view + "'" + '"></div>');
+          var view;
+          if (options.src)  {
+            view = angular.element(document.createElement('div'));
+            view.html('<div class="tabbed-view" ng-include="' + "'" + options.src + "'" + '"></div>');
+          } else {
+            view = angular.element(document.createElement('div'));
+            view.addClass('tabbed-view');
+            view.html(options.html);
           }
 
-          elem.append(view);
-          $compile(elem.contents())(editorScope);
+          $compile(view)(editorScope);
+
+          setTimeout(function() {
+            elem.empty();
+            elem.append(view);
+            setTimeout(function() {
+              elem.addClass('dash-edit-view--open');
+            }, 10);
+          }, 10);
         }
 
         scope.$watch("dashboardViewState.state.editview", function(newValue, oldValue) {
           if (newValue) {
-            showEditorPane(null, {}, newValue);
+            showEditorPane(null, {editview: newValue});
           } else if (oldValue) {
-            scope.contextSrv.editview = null;
-            if (lastEditor === editViewMap[oldValue]) {
+            if (lastEditView === oldValue) {
               hideEditorPane();
             }
           }
         });
 
-        scope.contextSrv.editview = null;
         scope.$on("$destroy", hideEditorPane);
-        scope.onAppEvent('hide-dash-editor', hideEditorPane);
+        scope.onAppEvent('hide-dash-editor', function() {
+          hideEditorPane(false);
+        });
+
         scope.onAppEvent('show-dash-editor', showEditorPane);
       }
     };

+ 226 - 0
public/app/core/nav_model_srv.ts

@@ -0,0 +1,226 @@
+///<reference path="../headers/common.d.ts" />
+
+import coreModule from 'app/core/core_module';
+
+export interface NavModelItem {
+  title: string;
+  url: string;
+  icon?: string;
+  iconUrl?: string;
+}
+
+export interface NavModel {
+  section: NavModelItem;
+  menu: NavModelItem[];
+}
+
+export class NavModelSrv {
+
+
+  /** @ngInject */
+  constructor(private contextSrv) {
+  }
+
+  getAlertingNav(subPage) {
+    return {
+      section: {
+        title: 'Alerting',
+        url: 'plugins',
+        icon: 'icon-gf icon-gf-alert'
+      },
+      menu: [
+        {title: 'Alert List', active: subPage === 0, url: 'alerting/list', icon: 'fa fa-list-ul'},
+        {title: 'Notification channels', active: subPage === 1, url: 'alerting/notifications', icon: 'fa fa-bell-o'},
+      ]
+    };
+  }
+
+  getDatasourceNav(subPage) {
+    return {
+      section: {
+        title: 'Data Sources',
+        url: 'datasources',
+        icon: 'icon-gf icon-gf-datasources'
+      },
+      menu: [
+        {title: 'List view', active: subPage === 0, url: 'datasources', icon: 'fa fa-list-ul'},
+        {title: 'Add data source', active: subPage === 1, url: 'datasources/new', icon: 'fa fa-plus'},
+      ]
+    };
+  }
+
+  getPlaylistsNav(subPage) {
+    return {
+      section: {
+        title: 'Playlists',
+        url: 'playlists',
+        icon: 'fa fa-fw fa-film'
+      },
+      menu: [
+        {title: 'List view', active: subPage === 0, url: 'playlists', icon: 'fa fa-list-ul'},
+        {title: 'Add Playlist', active: subPage === 1, url: 'playlists/create', icon: 'fa fa-plus'},
+      ]
+    };
+  }
+
+  getProfileNav() {
+    return {
+      section: {
+        title: 'User Profile',
+        url: 'profile',
+        icon: 'fa fa-fw fa-user'
+      },
+      menu: []
+    };
+  }
+
+  getNotFoundNav() {
+    return {
+      section: {
+        title: 'Page',
+        url: '',
+        icon: 'fa fa-fw fa-warning'
+      },
+      menu: []
+    };
+  }
+
+  getOrgNav(subPage) {
+    return {
+      section: {
+        title: 'Organization',
+        url: 'org',
+        icon: 'icon-gf icon-gf-users'
+      },
+      menu: [
+        {title: 'Preferences', active: subPage === 0, url: 'org', icon: 'fa fa-fw fa-cog'},
+        {title: 'Org Users', active: subPage === 1, url: 'org/users', icon: 'fa fa-fw fa-users'},
+        {title: 'API Keys', active: subPage === 2, url: 'org/apikeys', icon: 'fa fa-fw fa-key'},
+      ]
+    };
+  }
+
+  getAdminNav(subPage) {
+    return {
+      section: {
+        title: 'Admin',
+        url: 'admin',
+        icon: 'fa fa-fw fa-cogs'
+      },
+      menu: [
+        {title: 'Users', active: subPage === 0, url: 'admin/users', icon: 'fa fa-fw fa-user'},
+        {title: 'Orgs', active: subPage === 1, url: 'admin/orgs', icon: 'fa fa-fw fa-users'},
+        {title: 'Server Settings', active: subPage === 2, url: 'admin/settings', icon: 'fa fa-fw fa-cogs'},
+        {title: 'Server Stats', active: subPage === 2, url: 'admin/stats', icon: 'fa fa-fw fa-line-chart'},
+        {title: 'Style Guide', active: subPage === 2, url: 'styleguide', icon: 'fa fa-fw fa-key'},
+      ]
+    };
+  }
+
+  getPluginsNav() {
+    return {
+      section: {
+        title: 'Plugins',
+        url: 'plugins',
+        icon: 'icon-gf icon-gf-apps'
+      },
+      menu: []
+    };
+  }
+
+  getDashboardNav(dashboard, dashNavCtrl) {
+    // special handling for snapshots
+    if (dashboard.meta.isSnapshot) {
+      return {
+        section: {
+          title: dashboard.title,
+          icon: 'icon-gf icon-gf-snapshot'
+        },
+        menu: [
+          {
+            title: 'Go to original dashboard',
+            icon: 'fa fa-fw fa-external-link',
+            url: dashboard.snapshot.originalUrl,
+          }
+        ]
+      };
+    }
+
+    var menu = [];
+
+    if (dashboard.meta.canEdit) {
+      menu.push({
+        title: 'Settings',
+        icon: 'fa fa-fw fa-cog',
+        clickHandler: () => dashNavCtrl.openEditView('settings')
+      });
+
+      menu.push({
+        title: 'Templating',
+        icon: 'fa fa-fw fa-code',
+        clickHandler: () => dashNavCtrl.openEditView('templating')
+      });
+
+      menu.push({
+        title: 'Annotations',
+        icon: 'fa fa-fw fa-bolt',
+        clickHandler: () => dashNavCtrl.openEditView('annotations')
+      });
+
+      if (dashboard.version > 0) {
+        menu.push({
+          title: 'Version History',
+          icon: 'fa fa-fw fa-history',
+          clickHandler: () => dashNavCtrl.openEditView('history')
+        });
+      }
+
+      menu.push({
+        title: 'View JSON',
+        icon: 'fa fa-fw fa-eye',
+        clickHandler: () => dashNavCtrl.viewJson()
+      });
+    }
+
+    if (this.contextSrv.isEditor && !dashboard.editable) {
+      menu.push({
+        title: 'Make Editable',
+        icon: 'fa fa-fw fa-edit',
+        clickHandler: () => dashNavCtrl.makeEditable()
+      });
+    }
+
+    menu.push({
+      title: 'Shortcuts',
+      icon: 'fa fa-fw fa-keyboard-o',
+      clickHandler: () => dashNavCtrl.showHelpModal()
+    });
+
+    if (this.contextSrv.isEditor) {
+      menu.push({
+        title: 'Save As ...',
+        icon: 'fa fa-fw fa-save',
+        clickHandler: () => dashNavCtrl.saveDashboardAs()
+      });
+    }
+
+    if (dashboard.meta.canSave) {
+      menu.push({
+        title: 'Delete',
+        icon: 'fa fa-fw fa-trash',
+        clickHandler: () => dashNavCtrl.deleteDashboard()
+      });
+
+    }
+
+    return {
+      section: {
+        title: dashboard.title,
+        icon: 'icon-gf icon-gf-dashboard'
+      },
+      menu: menu
+    };
+  }
+}
+
+coreModule.service('navModelSrv', NavModelSrv);

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

@@ -103,11 +103,13 @@ function setupAngularRoutes($routeProvider, $locationProvider) {
   .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', {
@@ -129,11 +131,13 @@ function setupAngularRoutes($routeProvider, $locationProvider) {
   .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', {

+ 1 - 5
public/app/core/services/keybindingSrv.ts

@@ -113,10 +113,6 @@ export class KeybindingSrv {
       scope.appEvent('shift-time-forward');
     });
 
-    this.bind('mod+i', () => {
-      scope.appEvent('quick-snapshot');
-    });
-
     // edit panel
     this.bind('e', () => {
       if (dashboard.meta.focusPanelId && dashboard.meta.canEdit) {
@@ -225,7 +221,7 @@ export class KeybindingSrv {
       }
 
       scope.appEvent('hide-dash-editor');
-      scope.exitFullscreen();
+      scope.appEvent('panel-change-view', {fullscreen: false, edit: false});
     });
   }
 }

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

@@ -87,9 +87,11 @@ export default class TimeSeries {
       if (override.fill !== void 0) { this.lines.fill = translateFillOption(override.fill); }
       if (override.stack !== void 0) { this.stack = override.stack; }
       if (override.linewidth !== void 0) {
-         this.lines.lineWidth = override.linewidth;
+         this.lines.lineWidth = this.dashes.show ? 0: override.linewidth;
          this.dashes.lineWidth = override.linewidth;
       }
+      if (override.dashLength !== void 0) { this.dashes.dashLength[0] = override.dashLength; }
+      if (override.spaceLength !== void 0) { this.dashes.dashLength[1] = override.spaceLength; }
       if (override.nullPointMode !== void 0) { this.nullPointMode = override.nullPointMode; }
       if (override.pointradius !== void 0) { this.points.radius = override.pointradius; }
       if (override.steppedLine !== void 0) { this.lines.steps = override.steppedLine; }

+ 10 - 7
public/app/core/utils/file_export.ts

@@ -1,21 +1,24 @@
 ///<reference path="../../headers/common.d.ts" />
 
 import _ from 'lodash';
+import moment from 'moment';
 
 declare var window: any;
 
-export function exportSeriesListToCsv(seriesList) {
-    var text = 'sep=;\nSeries;Time;Value\n';
+const DEFAULT_DATETIME_FORMAT: String = 'YYYY-MM-DDTHH:mm:ssZ';
+
+export function exportSeriesListToCsv(seriesList, dateTimeFormat = DEFAULT_DATETIME_FORMAT) {
+    var text = 'Series;Time;Value\n';
     _.each(seriesList, function(series) {
         _.each(series.datapoints, function(dp) {
-            text += series.alias + ';' + new Date(dp[1]).toISOString() + ';' + dp[0] + '\n';
+            text += series.alias + ';' + moment(dp[1]).format(dateTimeFormat) + ';' + dp[0] + '\n';
         });
     });
     saveSaveBlob(text, 'grafana_data_export.csv');
 }
 
-export function exportSeriesListToCsvColumns(seriesList) {
-    var text = 'sep=;\nTime;';
+export function exportSeriesListToCsvColumns(seriesList, dateTimeFormat = DEFAULT_DATETIME_FORMAT) {
+    var text = 'Time;';
     // add header
     _.each(seriesList, function(series) {
         text += series.alias + ';';
@@ -30,7 +33,7 @@ export function exportSeriesListToCsvColumns(seriesList) {
         var cIndex = 0;
         dataArr.push([]);
         _.each(series.datapoints, function(dp) {
-            dataArr[0][cIndex] = new Date(dp[1]).toISOString();
+            dataArr[0][cIndex] = moment(dp[1]).format(dateTimeFormat);
             dataArr[sIndex][cIndex] = dp[0];
             cIndex++;
         });
@@ -50,7 +53,7 @@ export function exportSeriesListToCsvColumns(seriesList) {
 }
 
 export function exportTableDataToCsv(table) {
-    var text = 'sep=;\n';
+    var text = '';
     // add header
     _.each(table.columns, function(column) {
         text += (column.title || column.text) + ';';

+ 11 - 3
public/app/features/admin/admin.ts

@@ -6,9 +6,11 @@ import  './adminEditUserCtrl';
 import coreModule from 'app/core/core_module';
 
 class AdminSettingsCtrl {
+  navModel: any;
 
   /** @ngInject **/
-  constructor($scope, backendSrv) {
+  constructor($scope, backendSrv, navModelSrv) {
+    this.navModel = navModelSrv.getAdminNav();
 
     backendSrv.get('/api/admin/settings').then(function(settings) {
       $scope.settings = settings;
@@ -18,16 +20,22 @@ class AdminSettingsCtrl {
 }
 
 class AdminHomeCtrl {
+  navModel: any;
+
   /** @ngInject **/
-  constructor() {
+  constructor(navModelSrv) {
+    this.navModel = navModelSrv.getAdminNav();
   }
 }
 
 export class AdminStatsCtrl {
   stats: any;
+  navModel: any;
 
   /** @ngInject */
-  constructor(backendSrv: any) {
+  constructor(backendSrv: any, navModelSrv) {
+    this.navModel = navModelSrv.getAdminNav();
+
     backendSrv.get('/api/admin/stats').then(stats => {
       this.stats = stats;
     });

+ 3 - 1
public/app/features/admin/adminEditOrgCtrl.js

@@ -6,9 +6,11 @@ function (angular) {
 
   var module = angular.module('grafana.controllers');
 
-  module.controller('AdminEditOrgCtrl', function($scope, $routeParams, backendSrv, $location) {
+  module.controller('AdminEditOrgCtrl', function($scope, $routeParams, backendSrv, $location, navModelSrv) {
 
     $scope.init = function() {
+      $scope.navModel = navModelSrv.getAdminNav();
+
       if ($routeParams.id) {
         $scope.getOrg($routeParams.id);
         $scope.getOrgUsers($routeParams.id);

+ 2 - 1
public/app/features/admin/adminEditUserCtrl.js

@@ -7,10 +7,11 @@ function (angular, _) {
 
   var module = angular.module('grafana.controllers');
 
-  module.controller('AdminEditUserCtrl', function($scope, $routeParams, backendSrv, $location) {
+  module.controller('AdminEditUserCtrl', function($scope, $routeParams, backendSrv, $location, navModelSrv) {
     $scope.user = {};
     $scope.newOrg = { name: '', role: 'Editor' };
     $scope.permissions = {};
+    $scope.navModel = navModelSrv.getAdminNav();
 
     $scope.init = function() {
       if ($routeParams.id) {

+ 2 - 1
public/app/features/admin/adminListOrgsCtrl.js

@@ -6,9 +6,10 @@ function (angular) {
 
   var module = angular.module('grafana.controllers');
 
-  module.controller('AdminListOrgsCtrl', function($scope, backendSrv) {
+  module.controller('AdminListOrgsCtrl', function($scope, backendSrv, navModelSrv) {
 
     $scope.init = function() {
+      $scope.navModel = navModelSrv.getAdminNav();
       $scope.getOrgs();
     };
 

+ 3 - 1
public/app/features/admin/admin_list_users_ctrl.ts

@@ -8,9 +8,11 @@ export default class AdminListUsersCtrl {
   totalPages: number;
   showPaging = false;
   query: any;
+  navModel: any;
 
   /** @ngInject */
-  constructor(private $scope, private backendSrv) {
+  constructor(private $scope, private backendSrv, private navModelSrv) {
+    this.navModel = navModelSrv.getAdminNav();
     this.query = '';
     this.getUsers();
   }

+ 1 - 2
public/app/features/admin/partials/admin_home.html

@@ -1,5 +1,4 @@
-<navbar icon="fa fa-fw fa-cogs" title="Admin" title-url="admin">
-</navbar>
+<navbar model="ctrl.navModel"></navbar>
 
 <div class="page-container">
 	<div class="page-header">

+ 1 - 6
public/app/features/admin/partials/edit_org.html

@@ -1,9 +1,4 @@
-<navbar icon="fa fa-fw fa-cogs" title="Admin" title-url="admin">
-	<a href="admin/orgs" class="navbar-page-btn">
-		<i class="icon-gf icon-gf-users"></i>
-		Orgs
-	</a>
-</navbar>
+<navbar model="navModel"></navbar>
 
 <div class="page-container">
 	<div class="page-header">

+ 1 - 6
public/app/features/admin/partials/edit_user.html

@@ -1,9 +1,4 @@
-<navbar icon="fa fa-fw fa-cogs" title="Admin" title-url="admin">
-	<a href="admin/users" class="navbar-page-btn">
-		<i class="icon-gf icon-gf-users"></i>
-		Users
-	</a>
-</navbar>
+<navbar model="navModel"></navbar>
 
 <div class="page-container">
 	<div class="page-header">

+ 1 - 6
public/app/features/admin/partials/new_user.html

@@ -1,9 +1,4 @@
-<navbar icon="fa fa-fw fa-cogs" title="Admin" title-url="admin">
-	<a href="admin/users" class="navbar-page-btn">
-		<i class="icon-gf icon-gf-users"></i>
-		Users
-	</a>
-</navbar>
+<navbar model="navModel"></navbar>
 
 <div class="page-container">
 	<div class="page-header">

+ 1 - 6
public/app/features/admin/partials/orgs.html

@@ -1,9 +1,4 @@
-<navbar icon="fa fa-fw fa-cogs" title="Admin" title-url="admin">
-	<a href="admin/orgs" class="navbar-page-btn">
-		<i class="icon-gf icon-gf-users"></i>
-		Orgs
-	</a>
-</navbar>
+<navbar model="navModel"></navbar>
 
 <div class="page-container">
 	<div class="page-header">

+ 1 - 2
public/app/features/admin/partials/settings.html

@@ -1,5 +1,4 @@
-<navbar icon="fa fa-fw fa-cogs" title="Admin" title-url="admin">
-</navbar>
+<navbar model="ctrl.navModel"></navbar>
 
 <div class="page-container">
 	<div class="page-header">

+ 1 - 2
public/app/features/admin/partials/stats.html

@@ -1,5 +1,4 @@
-<navbar icon="fa fa-fw fa-cogs" title="Admin" title-url="admin">
-</navbar>
+<navbar model="ctrl.navModel"></navbar>
 
 <div class="page-container">
 	<div class="page-header">

+ 6 - 9
public/app/features/admin/partials/users.html

@@ -1,9 +1,4 @@
-<navbar icon="fa fa-fw fa-cogs" title="Admin" title-url="admin">
-  <a href="admin/users" class="navbar-page-btn">
-    <i class="icon-gf icon-gf-users"></i>
-    Users
-  </a>
-</navbar>
+<navbar model="ctrl.navModel"></navbar>
 
 <div class="page-container">
   <div class="page-header">
@@ -14,12 +9,14 @@
       Add new user
     </a>
   </div>
-  <div class="search-field-wrapper pull-right width-18">
+
+  <div class="gf-form pull-right gf-form-group">
+		<label class="gf-form-label">Search</label>
     <span style="position: relative;">
-      <input  type="text" placeholder="Find user by name/login/email" tabindex="1" give-focus="true"
-      ng-model="ctrl.query" ng-model-options="{ debounce: 500 }" spellcheck='false' ng-change="ctrl.getUsers()" />
+      <input class="gf-form-input width-15" type="text" placeholder="Find user by name/login/email" tabindex="1" give-focus="true" ng-model="ctrl.query" ng-model-options="{ debounce: 500 }" spellcheck='false' ng-change="ctrl.getUsers()" />
     </span>
   </div>
+
   <div class="admin-list-table">
     <table class="filter-table form-inline">
       <thead>

+ 6 - 5
public/app/features/alerting/alert_list_ctrl.ts

@@ -2,13 +2,12 @@
 
 import angular from 'angular';
 import _ from 'lodash';
-import coreModule from '../../core/core_module';
-import appEvents from '../../core/app_events';
 import moment from 'moment';
+
+import {coreModule, appEvents} from  'app/core/core';
 import alertDef from './alert_def';
 
 export class AlertListCtrl {
-
   alerts: any;
   stateFilters = [
     {text: 'All', value: null},
@@ -17,13 +16,15 @@ export class AlertListCtrl {
     {text: 'No Data', value: 'no_data'},
     {text: 'Paused', value: 'paused'},
   ];
-
   filters = {
     state: 'ALL'
   };
+  navModel: any;
 
   /** @ngInject */
-  constructor(private backendSrv, private $location, private $scope) {
+  constructor(private backendSrv, private $location, private $scope, navModelSrv) {
+    this.navModel = navModelSrv.getAlertingNav(0);
+
     var params = $location.search();
     this.filters.state = params.state || null;
     this.loadAlerts();

+ 4 - 1
public/app/features/alerting/notification_edit_ctrl.ts

@@ -7,6 +7,7 @@ import {appEvents, coreModule} from 'app/core/core';
 
 export class AlertNotificationEditCtrl {
   theForm: any;
+  navModel: any;
   testSeverity = "critical";
   notifiers: any;
   notifierTemplateId: string;
@@ -23,7 +24,9 @@ export class AlertNotificationEditCtrl {
   };
 
   /** @ngInject */
-  constructor(private $routeParams, private backendSrv, private $location, private $templateCache) {
+  constructor(private $routeParams, private backendSrv, private $location, private $templateCache, navModelSrv) {
+    this.navModel = navModelSrv.getAlertingNav();
+
     this.backendSrv.get(`/api/alert-notifiers`).then(notifiers => {
       this.notifiers = notifiers;
 

+ 6 - 3
public/app/features/alerting/notifications_list_ctrl.ts

@@ -2,16 +2,19 @@
 
 import angular from 'angular';
 import _ from 'lodash';
-import coreModule from '../../core/core_module';
 import config from 'app/core/config';
 
-export class AlertNotificationsListCtrl {
+import {coreModule} from  'app/core/core';
+
 
+export class AlertNotificationsListCtrl {
   notifications: any;
+  navModel: any;
 
   /** @ngInject */
-  constructor(private backendSrv, private $scope) {
+  constructor(private backendSrv, private $scope, navModelSrv) {
     this.loadNotifications();
+    this.navModel = navModelSrv.getAlertingNav(1);
   }
 
   loadNotifications() {

+ 1 - 2
public/app/features/alerting/partials/alert_list.html

@@ -1,5 +1,4 @@
-<navbar icon="icon-gf icon-gf-alert" title="Alerting" title-url="alerting">
-</navbar>
+<navbar model="ctrl.navModel"></navbar>
 
 <div class="page-container" >
 	<div class="page-header">

+ 1 - 6
public/app/features/alerting/partials/notification_edit.html

@@ -1,9 +1,4 @@
-<navbar icon="icon-gf icon-gf-alert" title="Alerting" title-url="alerting">
-	<a href="alerting/notifications" class="navbar-page-btn">
-		<i class="fa fa-fw fa-rss"></i>
-		Notification channels
-	</a>
-</navbar>
+<navbar model="ctrl.navModel"></navbar>
 
 <div class="page-container">
   <div class="page-header">

+ 1 - 2
public/app/features/alerting/partials/notifications_list.html

@@ -1,5 +1,4 @@
-<navbar icon="icon-gf icon-gf-alert" title="Alerting" title-url="alerting">
-</navbar>
+<navbar model="ctrl.navModel"></navbar>
 
 <div class="page-container" >
 	<div class="page-header">

+ 1 - 0
public/app/features/dashboard/all.js

@@ -19,6 +19,7 @@ define([
   './upload',
   './import/dash_import',
   './export/export_modal',
+  './export_data/export_data_modal',
   './dash_list_ctrl',
   './ad_hoc_filters',
   './row/row_ctrl',

+ 22 - 55
public/app/features/dashboard/dashnav/dashnav.html

@@ -1,95 +1,62 @@
-<navbar>
+<navbar model="ctrl.navModel">
 
-<a class="pointer navbar-page-btn" ng-if="::!dashboardMeta.isSnapshot" ng-click="openSearch()">
-	<i class="icon-gf icon-gf-dashboard"></i>
-	<span>{{dashboard.title}}</span>
-	<i class="fa fa-caret-down"></i>
-</a>
-
-<a class="pointer navbar-page-btn" ng-if="::dashboardMeta.isSnapshot" bs-tooltip="titleTooltip" data-placement="bottom" ng-click="openSearch()">
-	<i class="icon-gf icon-gf-snapshot"></i>
-	<span>
-		{{dashboard.title}}
-		<em class="small">&nbsp;&nbsp;(snapshot)</em>
-	</span>
-</a>
-
-<ul class="nav dash-playlist-actions" ng-if="playlistSrv">
+<ul class="nav dash-playlist-actions" ng-if="ctrl.playlistSrv.isPlaying">
 	<li>
-		<a ng-click="playlistSrv.prev()"><i class="fa fa-step-backward"></i></a>
+		<a ng-click="ctrl.playlistSrv.prev()"><i class="fa fa-step-backward"></i></a>
 	</li>
 	<li>
-		<a ng-click="playlistSrv.stop()"><i class="fa fa-stop"></i></a>
+		<a ng-click="ctrl.playlistSrv.stop()"><i class="fa fa-stop"></i></a>
 	</li>
 	<li>
-		<a ng-click="playlistSrv.next()"><i class="fa fa-step-forward"></i></a>
+		<a ng-click="ctrl.playlistSrv.next()"><i class="fa fa-step-forward"></i></a>
 	</li>
 </ul>
 
 <ul class="nav pull-left dashnav-action-icons">
-	<li ng-show="::dashboardMeta.canStar">
-		<a class="pointer" ng-click="starDashboard()">
-			<i class="fa" ng-class="{'fa-star-o': !dashboardMeta.isStarred, 'fa-star': dashboardMeta.isStarred}" style="color: orange;"></i>
+	<li ng-show="::ctrl.dashboard.meta.canStar">
+		<a class="pointer" ng-click="ctrl.starDashboard()">
+			<i class="fa" ng-class="{'fa-star-o': !ctrl.dashboard.meta.isStarred, 'fa-star': ctrl.dashboard.meta.isStarred}" style="color: orange;"></i>
 		</a>
 	</li>
-	<li ng-show="::dashboardMeta.canShare" class="dropdown">
-		<a class="pointer" ng-click="hideTooltip($event)" bs-tooltip="'Share dashboard'" data-placement="bottom" data-toggle="dropdown"><i class="fa fa-share-square-o"></i></a>
+	<li ng-show="::ctrl.dashboard.meta.canShare" class="dropdown">
+		<a class="pointer" ng-click="ctrl.hideTooltip($event)" bs-tooltip="'Share dashboard'" data-placement="bottom" data-toggle="dropdown"><i class="fa fa-share-square-o"></i></a>
 		<ul class="dropdown-menu">
 			<li>
-				<a class="pointer" ng-click="shareDashboard(0)">
+				<a class="pointer" ng-click="ctrl.shareDashboard(0)">
 					<i class="fa fa-link"></i> Link to Dashboard
 					<div class="dropdown-desc">Share an internal link to the current dashboard. Some configuration options available.</div>
 				</a>
 			</li>
 			<li>
-				<a class="pointer" ng-click="shareDashboard(1)">
+				<a class="pointer" ng-click="ctrl.shareDashboard(1)">
 					<i class="icon-gf icon-gf-snapshot"></i>Snapshot
 					<div class="dropdown-desc">Interactive, publically accessible dashboard. Sensitive data is stripped out.</div>
 				</a>
 			</li>
-      <li>
-				<a class="pointer" ng-click="shareDashboard(2)">
+			<li>
+				<a class="pointer" ng-click="ctrl.shareDashboard(2)">
 					<i class="fa fa-cloud-upload"></i>Export
 					<div class="dropdown-desc">Export the dashboard to a JSON file for others and to share on Grafana.com</div>
 				</a>
 			</li>
 		</ul>
 	</li>
-	<li ng-show="::dashboardMeta.canSave">
-		<a ng-show="dashboard.version === 0" ng-click="saveDashboard()" bs-tooltip="'Save dashboard <br> CTRL+S'" data-placement="bottom"><i class="fa fa-save"></i></a>
-		<a ng-show="dashboard.version > 0" ng-click="saveDashboard()" bs-tooltip="'Save to changelog <br> CTRL+S'" data-placement="bottom"><i class="fa fa-save"></i></a>
+	<li ng-show="::ctrl.dashboard.meta.canSave">
+		<a ng-click="ctrl.saveDashboard()" bs-tooltip="'Save dashboard <br> CTRL+S'" data-placement="bottom"><i class="fa fa-save"></i></a>
 	</li>
-	<li ng-if="dashboard.snapshot.originalUrl">
-		<a ng-href="{{dashboard.snapshot.originalUrl}}" bs-tooltip="'Open original dashboard'" data-placement="bottom"><i class="fa fa-link"></i></a>
-	</li>
-	<li ng-if="::showSettingsMenu" class="dropdown">
-		<a class="pointer" ng-click="hideTooltip($event)" bs-tooltip="'Manage dashboard'" data-placement="bottom" data-toggle="dropdown"><i class="fa fa-cog"></i></a>
-		<ul class="dropdown-menu">
-			<li ng-if="dashboardMeta.canEdit"><a class="pointer" ng-click="openEditView('settings');">Settings</a></li>
-			<li ng-if="dashboardMeta.canEdit"><a class="pointer" ng-click="openEditView('annotations');">Annotations</a></li>
-			<li ng-if="dashboardMeta.canEdit && dashboard.version > 0 && !dashboardMeta.isHome"><a class="pointer" ng-click="openEditView('history');">Changelog</a></li>
-			<li ng-if="dashboardMeta.canEdit"><a class="pointer" ng-click="openEditView('templating');">Templating</a></li>
-			<li ng-if="dashboardMeta.canEdit"><a class="pointer" ng-click="viewJson();">View JSON</a></li>
-			<li ng-if="contextSrv.isEditor && !dashboard.editable"><a class="pointer" ng-click="makeEditable();">Make Editable</a></li>
-			<li ng-if="contextSrv.isEditor"><a class="pointer" ng-click="saveDashboardAs();">Save As...</a></li>
-			<li class="dropdown-menu-item-with-shortcut">
-        <a class="pointer" ng-click="showHelpModal();">
-          Shortcuts <span class="dropdown-menu-item-shortcut">?</span>
-        </a>
-      </li>
-			<li ng-if="dashboardMeta.canSave"><a class="pointer" ng-click="deleteDashboard();">Delete dashboard</a></li>
-		</ul>
+	<li ng-if="::ctrl.dashboard.snapshot.originalUrl">
+		<a ng-href="{{ctrl.dashboard.snapshot.originalUrl}}" bs-tooltip="'Open original dashboard'" data-placement="bottom"><i class="fa fa-link"></i></a>
 	</li>
 </ul>
 
 <ul class="nav pull-right">
-	<li ng-show="dashboard.meta.fullscreen" class="dashnav-back-to-dashboard">
-		<a ng-click="exitFullscreen()">
+	<li ng-show="ctrl.dashboard.meta.fullscreen" class="dashnav-back-to-dashboard">
+		<a ng-click="ctrl.exitFullscreen()">
 			Back to dashboard
 		</a>
 	</li>
-	<li ng-if="dashboard">
-		<gf-time-picker dashboard="dashboard"></gf-time-picker>
+	<li>
+		<gf-time-picker dashboard="ctrl.dashboard"></gf-time-picker>
 	</li>
 </ul>
 

+ 82 - 96
public/app/features/dashboard/dashnav/dashnav.ts

@@ -3,92 +3,98 @@
 import _ from 'lodash';
 import moment from 'moment';
 import angular from 'angular';
-
+import {appEvents, NavModel} from 'app/core/core';
+import {DashboardModel} from '../model';
 import {DashboardExporter} from '../export/exporter';
 
 export class DashNavCtrl {
+  dashboard: DashboardModel;
+  navModel: NavModel;
+  titleTooltip: string;
 
   /** @ngInject */
-  constructor($scope, $rootScope, dashboardSrv, $location, playlistSrv, backendSrv, $timeout, datasourceSrv) {
-
-    $scope.init = function() {
-      $scope.onAppEvent('save-dashboard', $scope.saveDashboard);
-      $scope.onAppEvent('delete-dashboard', $scope.deleteDashboard);
-      $scope.onAppEvent('quick-snapshot', $scope.quickSnapshot);
-
-      $scope.showSettingsMenu = $scope.dashboardMeta.canEdit || $scope.contextSrv.isEditor;
-
-      if ($scope.dashboardMeta.isSnapshot) {
-        $scope.showSettingsMenu = false;
-        var meta = $scope.dashboardMeta;
-        $scope.titleTooltip = 'Created: &nbsp;' + moment(meta.created).calendar();
+  constructor(
+    private $scope,
+    private $rootScope,
+    private dashboardSrv,
+    private $location,
+    private playlistSrv,
+    private backendSrv,
+    private $timeout,
+    private datasourceSrv,
+    private navModelSrv) {
+
+      this.navModel = navModelSrv.getDashboardNav(this.dashboard, this);
+
+      appEvents.on('save-dashboard', this.saveDashboard.bind(this), $scope);
+      appEvents.on('delete-dashboard', this.deleteDashboard.bind(this), $scope);
+
+      if (this.dashboard.meta.isSnapshot) {
+        var meta = this.dashboard.meta;
+        this.titleTooltip = 'Created: &nbsp;' + moment(meta.created).calendar();
         if (meta.expires) {
-          $scope.titleTooltip += '<br>Expires: &nbsp;' + moment(meta.expires).fromNow() + '<br>';
+          this.titleTooltip += '<br>Expires: &nbsp;' + moment(meta.expires).fromNow() + '<br>';
         }
       }
-    };
+    }
 
-    $scope.openEditView = function(editview) {
-      var search = _.extend($location.search(), {editview: editview});
-      $location.search(search);
-    };
+    openEditView(editview) {
+      var search = _.extend(this.$location.search(), {editview: editview});
+      this.$location.search(search);
+    }
 
-    $scope.showHelpModal = function() {
-      $scope.appEvent('show-modal', {templateHtml: '<help-modal></help-modal>'});
-    };
+    showHelpModal() {
+      appEvents.emit('show-modal', {templateHtml: '<help-modal></help-modal>'});
+    }
 
-    $scope.starDashboard = function() {
-      if ($scope.dashboardMeta.isStarred) {
-        backendSrv.delete('/api/user/stars/dashboard/' + $scope.dashboard.id).then(function() {
-          $scope.dashboardMeta.isStarred = false;
-        });
-      } else {
-        backendSrv.post('/api/user/stars/dashboard/' + $scope.dashboard.id).then(function() {
-          $scope.dashboardMeta.isStarred = true;
+    starDashboard() {
+      if (this.dashboard.meta.isStarred) {
+        return this.backendSrv.delete('/api/user/stars/dashboard/' + this.dashboard.id).then(() =>  {
+          this.dashboard.meta.isStarred = false;
         });
       }
-    };
 
-    $scope.shareDashboard = function(tabIndex) {
-      var modalScope = $scope.$new();
+      this.backendSrv.post('/api/user/stars/dashboard/' + this.dashboard.id).then(() => {
+        this.dashboard.meta.isStarred = true;
+      });
+    }
+
+    shareDashboard(tabIndex) {
+      var modalScope = this.$scope.$new();
       modalScope.tabIndex = tabIndex;
+      modalScope.dashboard = this.dashboard;
 
-      $scope.appEvent('show-modal', {
+      appEvents.emit('show-modal', {
         src: 'public/app/features/dashboard/partials/shareModal.html',
         scope: modalScope
       });
-    };
-
-    $scope.quickSnapshot = function() {
-      $scope.shareDashboard(1);
-    };
+    }
 
-    $scope.openSearch = function() {
-      $scope.appEvent('show-dash-search');
-    };
-
-    $scope.hideTooltip = function(evt) {
+    hideTooltip(evt) {
       angular.element(evt.currentTarget).tooltip('hide');
-      $scope.appEvent('hide-dash-search');
-    };
+    }
 
-    $scope.makeEditable = function() {
-      $scope.dashboard.editable = true;
+    makeEditable() {
+      this.dashboard.editable = true;
 
-      return dashboardSrv.saveDashboard({makeEditable: true, overwrite: false}).then(function() {
+      return this.dashboardSrv.saveDashboard({makeEditable: true, overwrite: false}).then(() => {
         // force refresh whole page
         window.location.href = window.location.href;
       });
-    };
+    }
+
+    exitFullscreen() {
+      this.$rootScope.appEvent('panel-change-view', {fullscreen: false, edit: false});
+    }
 
-    $scope.saveDashboard = function(options) {
-      return dashboardSrv.saveDashboard(options);
-    };
+    saveDashboard(options) {
+      return this.dashboardSrv.saveDashboard(options);
+    }
 
-    $scope.deleteDashboard = function() {
+    deleteDashboard() {
       var confirmText = "";
-      var text2 = $scope.dashboard.title;
-      var alerts = $scope.dashboard.rows.reduce((memo, row) => {
+      var text2 = this.dashboard.title;
+      var alerts = this.dashboard.rows.reduce((memo, row) => {
         memo += row.panels.filter(panel => panel.alert).length;
         return memo;
       }, 0);
@@ -98,60 +104,37 @@ export class DashNavCtrl {
         text2 = `This dashboad contains ${alerts} alerts. Deleting this dashboad will also delete those alerts`;
       }
 
-      $scope.appEvent('confirm-modal', {
+      appEvents.emit('confirm-modal', {
         title: 'Delete',
         text: 'Do you want to delete this dashboard?',
         text2: text2,
         icon: 'fa-trash',
         confirmText: confirmText,
         yesText: 'Delete',
-        onConfirm: function() {
-          $scope.dashboardMeta.canSave = false;
-          $scope.deleteDashboardConfirmed();
+        onConfirm: () => {
+          this.dashboard.meta.canSave = false;
+          this.deleteDashboardConfirmed();
         }
       });
-    };
+    }
 
-    $scope.deleteDashboardConfirmed = function() {
-      backendSrv.delete('/api/dashboards/db/' + $scope.dashboardMeta.slug).then(function() {
-        $scope.appEvent('alert-success', ['Dashboard Deleted', $scope.dashboard.title + ' has been deleted']);
-        $location.url('/');
+    deleteDashboardConfirmed() {
+      this.backendSrv.delete('/api/dashboards/db/' + this.dashboard.meta.slug).then(() => {
+        appEvents.emit('alert-success', ['Dashboard Deleted', this.dashboard.title + ' has been deleted']);
+        this.$location.url('/');
       });
-    };
+    }
 
-    $scope.saveDashboardAs = function() {
-      return dashboardSrv.saveDashboardAs();
-    };
+    saveDashboardAs() {
+      return this.dashboardSrv.saveDashboardAs();
+    }
 
-    $scope.viewJson = function() {
-      var clone = $scope.dashboard.getSaveModelClone();
+    viewJson() {
+      var clone = this.dashboard.getSaveModelClone();
       var html = angular.toJson(clone, true);
       var uri = "data:application/json;charset=utf-8," + encodeURIComponent(html);
       var newWindow = window.open(uri);
-    };
-
-    $scope.snapshot = function() {
-      $scope.dashboard.snapshot = true;
-      $rootScope.$broadcast('refresh');
-
-      $timeout(function() {
-        $scope.dashboard.snapshot = false;
-        $scope.appEvent('dashboard-snapshot-cleanup');
-      }, 1000);
-
-    };
-
-    $scope.editJson = function() {
-      var clone = $scope.dashboard.getSaveModelClone();
-      $scope.appEvent('show-json-editor', { object: clone });
-    };
-
-    $scope.stopPlaylist = function() {
-      playlistSrv.stop(1);
-    };
-
-    $scope.init();
-  }
+    }
 }
 
 export function dashNavDirective() {
@@ -159,7 +142,10 @@ export function dashNavDirective() {
     restrict: 'E',
     templateUrl: 'public/app/features/dashboard/dashnav/dashnav.html',
     controller: DashNavCtrl,
+    bindToController: true,
+    controllerAs: 'ctrl',
     transclude: true,
+    scope: { dashboard: "=" }
   };
 }
 

+ 32 - 0
public/app/features/dashboard/export_data/export_data_modal.html

@@ -0,0 +1,32 @@
+<div class="modal-body">
+  <div class="modal-header">
+    <h2 class="modal-header-title">
+      Export CSV
+    </h2>
+
+    <a class="modal-header-close" ng-click="ctrl.dismiss();">
+      <i class="fa fa-remove"></i>
+    </a>
+  </div>
+
+  <div class="modal-content">
+    <div class="p-t-2">
+      <div class="gf-form">
+        <label class="gf-form-label width-10">Mode</label>
+        <div class="gf-form-select-wrapper">
+          <select class="gf-form-input" ng-model="ctrl.asRows" ng-options="f.value as f.text for f in [{text: 'Series as rows', value: true}, {text: 'Series as columns', value: false}]">
+          </select>
+        </div>
+      </div>
+      <div class="gf-form">
+        <label class="gf-form-label width-10">Date Time Format</label>
+        <input type="text" class="gf-form-input" ng-model="ctrl.dateTimeFormat">
+      </div>
+    </div>
+
+    <div class="gf-form-button-row text-center">
+      <a class="btn btn-success" ng-click="ctrl.export();">Export</a>
+      <a class="btn-text" ng-click="ctrl.dismiss();">Cancel</a>
+    </div>
+  </div>
+</div>

+ 41 - 0
public/app/features/dashboard/export_data/export_data_modal.ts

@@ -0,0 +1,41 @@
+///<reference path="../../../headers/common.d.ts" />
+
+import angular from 'angular';
+import * as fileExport from 'app/core/utils/file_export';
+import appEvents from 'app/core/app_events';
+
+export class ExportDataModalCtrl {
+  private data: any[];
+  asRows: Boolean = true;
+  dateTimeFormat: String = 'YYYY-MM-DDTHH:mm:ssZ';
+  /** @ngInject */
+  constructor(private $scope) { }
+
+  export() {
+    if (this.asRows) {
+      fileExport.exportSeriesListToCsv(this.data, this.dateTimeFormat);
+    } else {
+      fileExport.exportSeriesListToCsvColumns(this.data, this.dateTimeFormat);
+    }
+    this.dismiss();
+  }
+
+  dismiss() {
+    appEvents.emit('hide-modal');
+  }
+}
+
+export function exportDataModal() {
+  return {
+    restrict: 'E',
+    templateUrl: 'public/app/features/dashboard/export_data/export_data_modal.html',
+    controller: ExportDataModalCtrl,
+    controllerAs: 'ctrl',
+    scope: {
+      data: '<' // The difference to '=' is that the bound properties are not watched
+    },
+    bindToController: true
+  };
+}
+
+angular.module('grafana.directives').directive('exportDataModal', exportDataModal);

+ 128 - 132
public/app/features/dashboard/history/history.html

@@ -1,161 +1,157 @@
-<div>
-	<div class="tabbed-view-header">
-		<h2 class="tabbed-view-title">
-			Changelog
-		</h2>
+<div class="tabbed-view-header">
+	<h2 class="tabbed-view-title">
+		Changelog
+	</h2>
 
-		<ul class="gf-tabs">
-			<li class="gf-tabs-item" >
-				<a class="gf-tabs-link" ng-click="ctrl.mode = 'list';" ng-class="{active: ctrl.mode === 'list'}">
-					List
-				</a>
-			</li>
-			<li class="gf-tabs-item" ng-show="ctrl.mode === 'compare'">
-				<span ng-if="ctrl.isOriginalCurrent()" class="active gf-tabs-link">
-					Version {{ctrl.selected[0]}} <i class="fa fa-arrows-h"></i> Current
-				</span>
-				<span ng-if="!ctrl.isOriginalCurrent()" class="active gf-tabs-link">
-					Version {{ctrl.selected[0]}} <i class="fa fa-arrows-h"></i> Version {{ctrl.selected[1]}}
-				</span>
-			</li>
-		</ul>
+	<ul class="gf-tabs">
+		<li class="gf-tabs-item" >
+			<a class="gf-tabs-link" ng-click="ctrl.mode = 'list';" ng-class="{active: ctrl.mode === 'list'}">
+				List
+			</a>
+		</li>
+		<li class="gf-tabs-item" ng-show="ctrl.mode === 'compare'">
+			<span ng-if="ctrl.isOriginalCurrent()" class="active gf-tabs-link">
+				Version {{ctrl.selected[0]}} <i class="fa fa-arrows-h"></i> Current
+			</span>
+			<span ng-if="!ctrl.isOriginalCurrent()" class="active gf-tabs-link">
+				Version {{ctrl.selected[0]}} <i class="fa fa-arrows-h"></i> Version {{ctrl.selected[1]}}
+			</span>
+		</li>
+	</ul>
 
-		<button class="tabbed-view-close-btn" ng-click="dismiss();">
-			<i class="fa fa-remove"></i>
-		</button>
-	</div>
+	<button class="tabbed-view-close-btn" ng-click="ctrl.dismiss();">
+		<i class="fa fa-remove"></i>
+	</button>
+</div>
 
-	<div class="tabbed-view-body">
+<div class="tabbed-view-body">
 
-		<div ng-if="ctrl.mode === 'list'">
-			<div ng-if="ctrl.loading">
-				<i class="fa fa-spinner fa-spin"></i>
-				<em>Fetching history list&hellip;</em>
-			</div>
+	<div ng-if="ctrl.mode === 'list'">
+		<div ng-if="ctrl.loading">
+			<i class="fa fa-spinner fa-spin"></i>
+			<em>Fetching history list&hellip;</em>
+		</div>
 
-			<div ng-if="!ctrl.loading">
-				<div class="history-table gf-form">
-					<div class="gf-form-group">
-						<table class="filter-table">
-							<thead>
-								<tr>
-									<th class="width-4"></th>
-									<th class="width-4">Version</th>
-									<th class="width-14">Date</th>
-									<th class="width-10">Updated By</th>
-									<th class="width-30">Notes</th>
-									<th></th>
-								</tr>
-							</thead>
-							<tbody>
-								<tr ng-repeat="revision in ctrl.revisions">
-									<td bs-tooltip="ctrl.compareRevisionDisabled(revision.checked) ? 'You can only compare 2 versions at a time' : ''">
-										<gf-form-switch
-											checked="revision.checked"
-											on-change="ctrl.compareRevisionStateChanged(revision)"
-											ng-disabled="ctrl.compareRevisionDisabled(revision.checked)">
-										</gf-form-switch>
-									</td>
-									<td>{{revision.version}}</td>
-									<td>{{ctrl.formatDate(revision.created)}}</td>
-									<td>{{revision.createdBy}}</td>
-									<td>{{revision.message}}</td>
-									<td class="text-right">
-										<a class="btn btn-inverse btn-small" ng-show="revision.version !== ctrl.dashboard.version" ng-click="ctrl.restore(revision.version)">
-											<i class="fa fa-rotate-right"></i>&nbsp;&nbsp;Restore
-										</a>
-										<a class="btn btn-outline-disabled btn-small" ng-show="revision.version === ctrl.dashboard.version">
-											<i class="fa fa-check"></i>&nbsp;&nbsp;Current
-										</a>
-									</td>
-								</tr>
-							</tbody>
-						</table>
+		<div ng-if="!ctrl.loading">
+			<div class="history-table gf-form">
+				<div class="gf-form-group">
+					<table class="filter-table">
+						<thead>
+							<tr>
+								<th class="width-4"></th>
+								<th class="width-4">Version</th>
+								<th class="width-14">Date</th>
+								<th class="width-10">Updated By</th>
+								<th class="width-30">Notes</th>
+								<th></th>
+							</tr>
+						</thead>
+						<tbody>
+							<tr ng-repeat="revision in ctrl.revisions">
+								<td bs-tooltip="ctrl.compareRevisionDisabled(revision.checked) ? 'You can only compare 2 versions at a time' : ''">
+									<gf-form-switch checked="revision.checked"
+																	on-change="ctrl.compareRevisionStateChanged(revision)"
+																	ng-disabled="ctrl.compareRevisionDisabled(revision.checked)">
+									</gf-form-switch>
+								</td>
+								<td>{{revision.version}}</td>
+								<td>{{ctrl.formatDate(revision.created)}}</td>
+								<td>{{revision.createdBy}}</td>
+								<td>{{revision.message}}</td>
+								<td class="text-right">
+									<a class="btn btn-inverse btn-small" ng-show="revision.version !== ctrl.dashboard.version" ng-click="ctrl.restore(revision.version)">
+										<i class="fa fa-rotate-right"></i>&nbsp;&nbsp;Restore
+									</a>
+									<a class="btn btn-outline-disabled btn-small" ng-show="revision.version === ctrl.dashboard.version">
+										<i class="fa fa-check"></i>&nbsp;&nbsp;Current
+									</a>
+								</td>
+							</tr>
+						</tbody>
+					</table>
 
 					<div ng-if="ctrl.appending">
 						<i class="fa fa-spinner fa-spin"></i>
 						<em>Fetching more entries&hellip;</em>
 					</div>
 
-						<div class="gf-form-group" ng-show="ctrl.mode === 'list'">
-							<div class="gf-form-button-row">
-								<a	type="button"
-										class="btn gf-form-button btn-primary"
-										ng-if="ctrl.revisions.length > 1"
-										ng-class="{disabled: !ctrl.isComparable()}"
-										ng-click="ctrl.getDiff(ctrl.diff)"
-										bs-tooltip="ctrl.isComparable() ? '' : 'Select 2 versions to start comparing'">
-									<i class="fa fa-code-fork" ></i>&nbsp;&nbsp;Compare versions
-								</a>
-								<a  type="button"
-										class="btn gf-form-button btn-inverse"
-										ng-if="ctrl.revisions.length >= ctrl.limit"
-										ng-click="ctrl.addToLog()"
-										ng-class="{disabled: ctrl.isLastPage()}"
-										ng-disabled="ctrl.isLastPage()">
-									Show more versions
-								</a>
-							</div>
+					<div class="gf-form-group" ng-show="ctrl.mode === 'list'">
+						<div class="gf-form-button-row">
+							<a	type="button"
+									class="btn gf-form-button btn-primary"
+									ng-if="ctrl.revisions.length > 1"
+									ng-class="{disabled: !ctrl.isComparable()}"
+									ng-click="ctrl.getDiff(ctrl.diff)"
+									bs-tooltip="ctrl.isComparable() ? '' : 'Select 2 versions to start comparing'">
+								<i class="fa fa-code-fork" ></i>&nbsp;&nbsp;Compare versions
+							</a>
+							<a  type="button"
+									class="btn gf-form-button btn-inverse"
+									ng-if="ctrl.revisions.length >= ctrl.limit"
+									ng-click="ctrl.addToLog()"
+									ng-class="{disabled: ctrl.isLastPage()}"
+									ng-disabled="ctrl.isLastPage()">
+								Show more versions
+							</a>
 						</div>
 					</div>
 				</div>
 			</div>
 		</div>
+	</div>
 
-		<div class="history-list" ng-if="ctrl.mode === 'compare'">
-			<div class="page-container">
-				<div class="page-body">
-					<aside class="page-sidebar">
-						<section class="page-sidebar-section">
-							<ul class="ui-list">
-								<li><a ng-class="{active: ctrl.diff === 'basic'}" ng-click="ctrl.getDiff('basic')" href="">Change Summary</a></li>
-								<li><a ng-class="{active: ctrl.diff === 'html'}" ng-click="ctrl.getDiff('html')" href="">JSON Code View</a></li>
-							</ul>
-						</section>
-					</aside>
+	<div class="history-list" ng-if="ctrl.mode === 'compare'">
+		<div class="page-container">
+			<div class="page-body">
+				<aside class="page-sidebar">
+					<section class="page-sidebar-section">
+						<ul class="ui-list">
+							<li><a ng-class="{active: ctrl.diff === 'basic'}" ng-click="ctrl.getDiff('basic')" href="">Change Summary</a></li>
+							<li><a ng-class="{active: ctrl.diff === 'html'}" ng-click="ctrl.getDiff('html')" href="">JSON Code View</a></li>
+						</ul>
+					</section>
+				</aside>
 
-					<div class="tab-content page-content-with-sidebar">
-						<div ng-if="ctrl.loading">
-							<i class="fa fa-spinner fa-spin"></i>
-							<em>Fetching changes&hellip;</em>
-						</div>
+				<div class="tab-content page-content-with-sidebar">
+					<div ng-if="ctrl.loading">
+						<i class="fa fa-spinner fa-spin"></i>
+						<em>Fetching changes&hellip;</em>
+					</div>
 
-						<div ng-if="!ctrl.loading" ng-init="new = ctrl.selected[0]; original = ctrl.selected[1]">
-							<a type="button"
+					<div ng-if="!ctrl.loading" ng-init="new = ctrl.selected[0]; original = ctrl.selected[1]">
+						<a  type="button"
 								class="btn gf-form-button btn-primary diff-restore-btn"
 								ng-click="ctrl.restore(new)"
 								ng-if="ctrl.isOriginalCurrent()">
-								<i class="fa fa-rotate-right" ></i>&nbsp;&nbsp;Restore to version {{new}}
-							</a>
-							<h4>
-								Comparing Version {{ctrl.selected[0]}}
-								<i class="fa fa-arrows-h"></i>
-								Version {{ctrl.selected[1]}}
-								<cite class="muted" ng-if="ctrl.isOriginalCurrent()">(Current)</cite>
-							</h4>
-							<section ng-if="ctrl.diff === 'basic'">
-								<p class="small muted">
-									<strong>Version {{new}}</strong> updated by
-									<span>{{ctrl.getMeta(new, 'createdBy')}} </span>
-									<span>{{ctrl.formatBasicDate(ctrl.getMeta(new, 'created'))}}</span>
-									<span> - {{ctrl.getMeta(new, 'message')}}</span>
-								</p>
-								<p class="small muted">
-									<strong>Version {{original}}</strong> updated by
-									<span>{{ctrl.getMeta(original, 'createdBy')}} </span>
-									<span>{{ctrl.formatBasicDate(ctrl.getMeta(original, 'created'))}}</span>
-									<span> - {{ctrl.getMeta(original, 'message')}}</span>
-								</p>
-							</section>
-							<div id="delta" diff-delta>
-								<div class="delta-basic" ng-show="ctrl.diff === 'basic'" compile="ctrl.delta.basic"></div>
-								<div class="delta-html" ng-show="ctrl.diff === 'html'" compile="ctrl.delta.html"></div>
-							</div>
+							<i class="fa fa-rotate-right" ></i>&nbsp;&nbsp;Restore to version {{new}}
+						</a>
+						<h4>
+							Comparing Version {{ctrl.selected[0]}}
+							<i class="fa fa-arrows-h"></i>
+							Version {{ctrl.selected[1]}}
+							<cite class="muted" ng-if="ctrl.isOriginalCurrent()">(Current)</cite>
+						</h4>
+						<section ng-if="ctrl.diff === 'basic'">
+							<p class="small muted">
+							<strong>Version {{new}}</strong> updated by
+							<span>{{ctrl.getMeta(new, 'createdBy')}} </span>
+							<span>{{ctrl.formatBasicDate(ctrl.getMeta(new, 'created'))}}</span>
+							<span> - {{ctrl.getMeta(new, 'message')}}</span>
+							</p>
+							<p class="small muted">
+							<strong>Version {{original}}</strong> updated by
+							<span>{{ctrl.getMeta(original, 'createdBy')}} </span>
+							<span>{{ctrl.formatBasicDate(ctrl.getMeta(original, 'created'))}}</span>
+							<span> - {{ctrl.getMeta(original, 'message')}}</span>
+							</p>
+						</section>
+						<div id="delta" diff-delta>
+							<div class="delta-basic" ng-show="ctrl.diff === 'basic'" compile="ctrl.delta.basic"></div>
+							<div class="delta-html" ng-show="ctrl.diff === 'html'" compile="ctrl.delta.html"></div>
 						</div>
 					</div>
 				</div>
 			</div>
 		</div>
-
 	</div>
 </div>

+ 4 - 0
public/app/features/dashboard/history/history.ts

@@ -54,6 +54,10 @@ export class HistoryListCtrl {
     $rootScope.onAppEvent('dashboard-saved', this.onDashboardSaved.bind(this));
   }
 
+  dismiss() {
+    this.$rootScope.appEvent('hide-dash-editor');
+  }
+
   addToLog() {
     this.start = this.start + this.limit;
     this.getLog(true);

+ 0 - 1
public/app/features/dashboard/history/partials/history.html

@@ -1 +0,0 @@
-<gf-dashboard-history dashboard="dashboard"></gf-dashboard-history>

+ 2 - 2
public/app/features/dashboard/partials/saveDashboardMessage.html

@@ -2,7 +2,7 @@
 	<div class="modal-header">
 		<h2 class="modal-header-title">
 			<i class="fa fa-save"></i>
-			<span class="p-l-1">Save to changelog</span>
+			<span class="p-l-1">Save Dashboard</span>
 		</h2>
 
 		<a class="modal-header-close" ng-click="dismiss();">
@@ -39,7 +39,7 @@
 
 		<div class="gf-form-button-row text-center">
 			<button type="submit" class="btn btn-success" ng-disabled="saveMessage.$invalid">
-				Save to changelog
+				Save
 			</button>
 			<button class="btn btn-inverse" ng-click="dismiss();">Cancel</button>
 		</div>

+ 1 - 2
public/app/features/dashboard/timepicker/timepicker.ts

@@ -34,7 +34,6 @@ export class TimePickerCtrl {
     $rootScope.onAppEvent('shift-time-backward', () => this.move(-1), $scope);
     $rootScope.onAppEvent('refresh', () => this.init(), $scope);
     $rootScope.onAppEvent('dash-editor-hidden', () => this.isOpen = false, $scope);
-
     this.init();
   }
 
@@ -114,7 +113,7 @@ export class TimePickerCtrl {
     this.refresh.options.unshift({text: 'off'});
 
     this.$rootScope.appEvent('show-dash-editor', {
-      src: 'public/app/features/dashboard/timepicker/dropdown.html',
+      editview: 'timepicker',
       scope: this.$scope,
       cssClass: 'gf-timepicker-dropdown',
     });

+ 3 - 6
public/app/features/dashboard/viewStateSrv.js

@@ -20,12 +20,6 @@ function (angular, _, $, config) {
       self.$scope = $scope;
       self.dashboard = $scope.dashboard;
 
-      $scope.exitFullscreen = function() {
-        if (self.state.fullscreen) {
-          self.update({ fullscreen: false });
-        }
-      };
-
       $scope.onAppEvent('$routeUpdate', function() {
         var urlState = self.getQueryStringState();
         if (self.needsSync(urlState)) {
@@ -41,6 +35,9 @@ function (angular, _, $, config) {
         self.registerPanel(payload.scope);
       });
 
+      // this marks changes to location during this digest cycle as not to add history item
+      // dont want url changes like adding orgId to add browser history
+      $location.replace();
       this.update(this.getQueryStringState());
       this.expandRowForPanel();
     }

+ 2 - 1
public/app/features/org/change_password_ctrl.js

@@ -7,11 +7,12 @@ function (angular, config) {
 
   var module = angular.module('grafana.controllers');
 
-  module.controller('ChangePasswordCtrl', function($scope, backendSrv, $location) {
+  module.controller('ChangePasswordCtrl', function($scope, backendSrv, $location, navModelSrv) {
 
     $scope.command = {};
     $scope.authProxyEnabled = config.authProxyEnabled;
     $scope.ldapEnabled = config.ldapEnabled;
+    $scope.navModel = navModelSrv.getProfileNav();
 
     $scope.changePassword = function() {
       if (!$scope.userForm.$valid) { return; }

+ 2 - 1
public/app/features/org/newOrgCtrl.js

@@ -7,8 +7,9 @@ function (angular, config) {
 
   var module = angular.module('grafana.controllers');
 
-  module.controller('NewOrgCtrl', function($scope, $http, backendSrv) {
+  module.controller('NewOrgCtrl', function($scope, $http, backendSrv, navModelSrv) {
 
+    $scope.navModel = navModelSrv.getOrgNav(0);
     $scope.newOrg = {name: ''};
 
     $scope.createOrg = function() {

+ 2 - 1
public/app/features/org/orgApiKeysCtrl.js

@@ -6,8 +6,9 @@ function (angular) {
 
   var module = angular.module('grafana.controllers');
 
-  module.controller('OrgApiKeysCtrl', function($scope, $http, backendSrv) {
+  module.controller('OrgApiKeysCtrl', function($scope, $http, backendSrv, navModelSrv) {
 
+    $scope.navModel = navModelSrv.getOrgNav(0);
     $scope.roleTypes = ['Viewer', 'Editor', 'Admin'];
     $scope.token = { role: 'Viewer' };
 

+ 2 - 1
public/app/features/org/orgDetailsCtrl.js

@@ -6,10 +6,11 @@ function (angular) {
 
   var module = angular.module('grafana.controllers');
 
-  module.controller('OrgDetailsCtrl', function($scope, $http, backendSrv, contextSrv) {
+  module.controller('OrgDetailsCtrl', function($scope, $http, backendSrv, contextSrv, navModelSrv) {
 
     $scope.init = function() {
       $scope.getOrgInfo();
+      $scope.navModel = navModelSrv.getOrgNav(0);
     };
 
     $scope.getOrgInfo = function() {

+ 3 - 1
public/app/features/org/org_users_ctrl.ts

@@ -11,13 +11,15 @@ export class OrgUsersCtrl {
   pendingInvites: any;
   editor: any;
   showInviteUI: boolean;
+  navModel: any;
 
   /** @ngInject */
-  constructor(private $scope, private $http, private backendSrv) {
+  constructor(private $scope, private $http, private backendSrv, navModelSrv) {
     this.user = {
       loginOrEmail: '',
       role: 'Viewer',
     };
+    this.navModel = navModelSrv.getOrgNav(0);
 
     this.get();
     this.editor = { index: 0 };

+ 1 - 2
public/app/features/org/partials/change_password.html

@@ -1,5 +1,4 @@
-<navbar icon="icon-gf icon-gf-users" title="Profile" title-url="profile">
-</navbar>
+<navbar model="navModel"></navbar>
 
 <div class="page-container">
 	<div class="page-header">

+ 1 - 2
public/app/features/org/partials/newOrg.html

@@ -1,5 +1,4 @@
-<navbar title="Organization" icon="icon-gf icon-gf-users">
-</navbar>
+<navbar model="navModel"></navbar>
 
 <div class="page-container" ng-form="playlistEditForm">
 	<div class="page-header">

+ 1 - 2
public/app/features/org/partials/orgApiKeys.html

@@ -1,5 +1,4 @@
-<navbar icon="icon-gf icon-gf-users" title="Organization" title-url="org">
-</navbar>
+<navbar model="navModel"></navbar>
 
 <div class="page-container">
 	<div class="page-header">

+ 1 - 2
public/app/features/org/partials/orgDetails.html

@@ -1,5 +1,4 @@
-<navbar icon="icon-gf icon-gf-users" title="Organization" title-url="org">
-</navbar>
+<navbar model="navModel"></navbar>
 
 <div class="page-container">
 	<div class="page-header">

+ 1 - 2
public/app/features/org/partials/orgUsers.html

@@ -1,5 +1,4 @@
-<navbar icon="icon-gf icon-gf-users" title="Organization Users" title-url="org/users">
-</navbar>
+<navbar model="ctrl.navModel"></navbar>
 
 <div class="page-container">
 	<div class="page-header">

+ 2 - 3
public/app/features/org/partials/profile.html

@@ -1,9 +1,8 @@
-<navbar icon="icon-gf icon-gf-users" title="Profile" title-url="profile">
-</navbar>
+<navbar model="ctrl.navModel"></navbar>
 
 <div class="page-container">
 	<div class="page-header">
-		<h1>Profile</h1>
+		<h1>User Profile</h1>
 	</div>
 
 	<form name="ctrl.userForm" class="gf-form-group">

+ 3 - 1
public/app/features/org/profile_ctrl.ts

@@ -11,11 +11,13 @@ export class ProfileCtrl {
   userForm: any;
   showOrgsList = false;
   readonlyLoginFields = config.disableLoginForm;
+  navModel: any;
 
   /** @ngInject **/
-  constructor(private backendSrv, private contextSrv, private $location) {
+  constructor(private backendSrv, private contextSrv, private $location, navModelSrv) {
     this.getUser();
     this.getUserOrgs();
+    this.navModel = navModelSrv.getProfileNav();
   }
 
   getUser() {

+ 0 - 9
public/app/features/panel/panel_ctrl.ts

@@ -35,8 +35,6 @@ export class PanelCtrl {
   containerHeight: any;
   events: Emitter;
   timing: any;
-  skippedLastRefresh: boolean;
-  isPanelVisible: any;
 
   constructor($scope, $injector) {
     this.$injector = $injector;
@@ -77,13 +75,6 @@ export class PanelCtrl {
   }
 
   refresh() {
-    if (!this.isPanelVisible() && !this.dashboard.meta.soloMode && !this.dashboard.snapshot) {
-      this.skippedLastRefresh = true;
-      return;
-    }
-
-    this.skippedLastRefresh = false;
-
     this.events.emit('refresh', null);
   }
 

+ 1 - 14
public/app/features/panel/panel_directive.ts

@@ -4,6 +4,7 @@ import angular from 'angular';
 import $ from 'jquery';
 import _ from 'lodash';
 import Drop from 'tether-drop';
+import {appEvents} from 'app/core/core';
 
 var module = angular.module('grafana.directives');
 
@@ -185,23 +186,9 @@ module.directive('grafanaPanel', function($rootScope, $document) {
       elem.on('mouseenter', mouseEnter);
       elem.on('mouseleave', mouseLeave);
 
-      ctrl.isPanelVisible = function () {
-        var position = panelContainer[0].getBoundingClientRect();
-        return (0 < position.top) && (position.top < window.innerHeight);
-      };
-
-      const refreshOnScroll = function () {
-        if (ctrl.skippedLastRefresh) {
-          ctrl.refresh();
-        }
-      };
-
-      $document.on('scroll', refreshOnScroll);
-
       scope.$on('$destroy', function() {
         elem.off();
         cornerInfoElem.off();
-        $document.off('scroll', refreshOnScroll);
 
         if (infoDrop) {
           infoDrop.destroy();

+ 1 - 2
public/app/features/playlist/partials/playlist.html

@@ -1,5 +1,4 @@
-<navbar icon="fa fa-fw fa-list" title="Playlists" title-url="playlists">
-</navbar>
+<navbar model="ctrl.navModel"></navbar>
 
 <div class="page-container" ng-form="playlistEditForm">
 	<div class="page-header">

+ 1 - 2
public/app/features/playlist/partials/playlists.html

@@ -1,5 +1,4 @@
-<navbar icon="fa fa-fw fa-list" title="Playlists" title-url="playlists">
-</navbar>
+<navbar model="ctrl.navModel"></navbar>
 
 <div class="page-container">
   <div class="page-header">

+ 19 - 10
public/app/features/playlist/playlist_edit_ctrl.ts

@@ -16,21 +16,30 @@ export class PlaylistEditCtrl {
   playlistItems: any = [];
   dashboardresult: any = [];
   tagresult: any = [];
+  navModel: any;
 
   /** @ngInject */
-  constructor(private $scope, private playlistSrv, private backendSrv, private $location, private $route) {
+  constructor(
+    private $scope,
+    private playlistSrv,
+    private backendSrv,
+    private $location,
+    private $route,
+    private navModelSrv
+  ) {
+
+    this.navModel = navModelSrv.getPlaylistsNav(0);
+
     if ($route.current.params.id) {
       var playlistId = $route.current.params.id;
 
-      backendSrv.get('/api/playlists/' + playlistId)
-        .then((result) => {
-          this.playlist = result;
-        });
+      backendSrv.get('/api/playlists/' + playlistId).then(result => {
+        this.playlist = result;
+      });
 
-      backendSrv.get('/api/playlists/' + playlistId + '/items')
-        .then((result) => {
-          this.playlistItems = result;
-        });
+      backendSrv.get('/api/playlists/' + playlistId + '/items').then(result => {
+        this.playlistItems = result;
+      });
     }
   }
 
@@ -85,7 +94,7 @@ export class PlaylistEditCtrl {
       ? this.backendSrv.put('/api/playlists/' + playlist.id, playlist)
       : this.backendSrv.post('/api/playlists', playlist);
 
-    savePromise
+      savePromise
       .then(() => {
         this.$scope.appEvent('alert-success', ['Playlist saved', '']);
         this.$location.path('/playlists');

+ 3 - 3
public/app/features/playlist/playlist_srv.ts

@@ -11,6 +11,7 @@ class PlaylistSrv {
   private interval: any;
   private playlistId: number;
   private startUrl: string;
+  public isPlaying: boolean;
 
   /** @ngInject */
   constructor(private $rootScope: any, private $location: any, private $timeout: any, private backendSrv: any) { }
@@ -42,7 +43,7 @@ class PlaylistSrv {
     this.startUrl = window.location.href;
     this.index = 0;
     this.playlistId = playlistId;
-    this.$rootScope.playlistSrv = this;
+    this.isPlaying = true;
 
     this.backendSrv.get(`/api/playlists/${playlistId}`).then(playlist => {
       this.backendSrv.get(`/api/playlists/${playlistId}/dashboards`).then(dashboards => {
@@ -55,13 +56,12 @@ class PlaylistSrv {
 
   stop() {
     this.index = 0;
+    this.isPlaying = false;
     this.playlistId = 0;
 
     if (this.cancelPromise) {
       this.$timeout.cancel(this.cancelPromise);
     }
-
-    this.$rootScope.playlistSrv = null;
   }
 }
 

+ 8 - 6
public/app/features/playlist/playlists_ctrl.ts

@@ -6,20 +6,22 @@ import coreModule from '../../core/core_module';
 
 export class PlaylistsCtrl {
   playlists: any;
+  navModel: any;
 
   /** @ngInject */
-  constructor(private $scope, private $location, private backendSrv) {
-    backendSrv.get('/api/playlists')
-      .then((result) => {
-        this.playlists = result;
-      });
+  constructor(private $scope, private $location, private backendSrv, private navModelSrv) {
+    this.navModel = navModelSrv.getPlaylistsNav(0);
+
+    backendSrv.get('/api/playlists').then(result => {
+      this.playlists = result;
+    });
   }
 
   removePlaylistConfirmed(playlist) {
     _.remove(this.playlists, { id: playlist.id });
 
     this.backendSrv.delete('/api/playlists/' + playlist.id)
-      .then(() => {
+    .then(() => {
         this.$scope.appEvent('alert-success', ['Playlist deleted', '']);
       }, () => {
         this.$scope.appEvent('alert-error', ['Unable to delete playlist', '']);

+ 5 - 1
public/app/features/playlist/specs/playlist_edit_ctrl_specs.ts

@@ -5,7 +5,11 @@ import {PlaylistEditCtrl} from '../playlist_edit_ctrl';
 describe('PlaylistEditCtrl', () => {
   var ctx: any;
   beforeEach(() => {
-    ctx = new PlaylistEditCtrl(null, null, null, null, { current: { params: {} } });
+    let navModelSrv = {
+      getPlaylistsNav: page => {},
+    };
+
+    ctx = new PlaylistEditCtrl(null, null, null, null, { current: { params: {} } }, navModelSrv);
 
     ctx.dashboardresult = [
       { id: 2, title: 'dashboard: 2' },

+ 120 - 116
public/app/features/plugins/ds_edit_ctrl.ts

@@ -30,6 +30,7 @@ export class DataSourceEditCtrl {
   hasDashboards: boolean;
   editForm: any;
   gettingStarted: boolean;
+  navModel: any;
 
   /** @ngInject */
   constructor(
@@ -38,141 +39,144 @@ export class DataSourceEditCtrl {
     private backendSrv,
     private $routeParams,
     private $location,
-    private datasourceSrv) {
-
-      this.isNew = true;
-      this.datasources = [];
-      this.tabIndex = 0;
-
-      this.loadDatasourceTypes().then(() => {
-        if (this.$routeParams.id) {
-          this.getDatasourceById(this.$routeParams.id);
-        } else {
-          this.initNewDatasourceModel();
-        }
-      });
-    }
-
-    initNewDatasourceModel() {
-      this.current = angular.copy(defaults);
-
-      // We are coming from getting started
-      if (this.$location.search().gettingstarted) {
-        this.gettingStarted = true;
-        this.current.isDefault = true;
-      }
-
-      this.typeChanged();
-    }
-
-    loadDatasourceTypes() {
-      if (datasourceTypes.length > 0) {
-        this.types = datasourceTypes;
-        return this.$q.when(null);
+    private datasourceSrv,
+    private navModelSrv,
+  ) {
+
+    this.navModel = navModelSrv.getDatasourceNav(0);
+    this.isNew = true;
+    this.datasources = [];
+    this.tabIndex = 0;
+
+    this.loadDatasourceTypes().then(() => {
+      if (this.$routeParams.id) {
+        this.getDatasourceById(this.$routeParams.id);
+      } else {
+        this.initNewDatasourceModel();
       }
+    });
+  }
 
-      return this.backendSrv.get('/api/plugins', {enabled: 1, type: 'datasource'}).then(plugins => {
-        datasourceTypes = plugins;
-        this.types = plugins;
-      });
-    }
+  initNewDatasourceModel() {
+    this.current = angular.copy(defaults);
 
-    getDatasourceById(id) {
-      this.backendSrv.get('/api/datasources/' + id).then(ds => {
-        this.isNew = false;
-        this.current = ds;
-        if (datasourceCreated) {
-          datasourceCreated = false;
-          this.testDatasource();
-        }
-        return this.typeChanged();
-      });
+    // We are coming from getting started
+    if (this.$location.search().gettingstarted) {
+      this.gettingStarted = true;
+      this.current.isDefault = true;
     }
 
-    typeChanged() {
-      this.hasDashboards = false;
-      return this.backendSrv.get('/api/plugins/' + this.current.type + '/settings').then(pluginInfo => {
-        this.datasourceMeta = pluginInfo;
-        console.log(this.datasourceMeta) ;
-        this.hasDashboards = _.find(pluginInfo.includes, {type: 'dashboard'});
-      });
-    }
+    this.typeChanged();
+  }
 
-    updateFrontendSettings() {
-      return this.backendSrv.get('/api/frontend/settings').then(settings => {
-        config.datasources = settings.datasources;
-        config.defaultDatasource = settings.defaultDatasource;
-        this.datasourceSrv.init();
-      });
+  loadDatasourceTypes() {
+    if (datasourceTypes.length > 0) {
+      this.types = datasourceTypes;
+      return this.$q.when(null);
     }
 
-    testDatasource() {
-      this.testing = { done: false };
-
-      this.datasourceSrv.get(this.current.name).then(datasource => {
-        if (!datasource.testDatasource) {
-          delete this.testing;
-          return;
-        }
+    return this.backendSrv.get('/api/plugins', {enabled: 1, type: 'datasource'}).then(plugins => {
+      datasourceTypes = plugins;
+      this.types = plugins;
+    });
+  }
+
+  getDatasourceById(id) {
+    this.backendSrv.get('/api/datasources/' + id).then(ds => {
+      this.isNew = false;
+      this.current = ds;
+      if (datasourceCreated) {
+        datasourceCreated = false;
+        this.testDatasource();
+      }
+      return this.typeChanged();
+    });
+  }
+
+  typeChanged() {
+    this.hasDashboards = false;
+    return this.backendSrv.get('/api/plugins/' + this.current.type + '/settings').then(pluginInfo => {
+      this.datasourceMeta = pluginInfo;
+      console.log(this.datasourceMeta) ;
+      this.hasDashboards = _.find(pluginInfo.includes, {type: 'dashboard'});
+    });
+  }
+
+  updateFrontendSettings() {
+    return this.backendSrv.get('/api/frontend/settings').then(settings => {
+      config.datasources = settings.datasources;
+      config.defaultDatasource = settings.defaultDatasource;
+      this.datasourceSrv.init();
+    });
+  }
+
+  testDatasource() {
+    this.testing = { done: false };
+
+    this.datasourceSrv.get(this.current.name).then(datasource => {
+      if (!datasource.testDatasource) {
+        delete this.testing;
+        return;
+      }
 
-        return datasource.testDatasource().then(result => {
-          this.testing.message = result.message;
-          this.testing.status = result.status;
-          this.testing.title = result.title;
-        }).catch(err => {
-          if (err.statusText) {
-            this.testing.message = err.statusText;
-            this.testing.title = "HTTP Error";
-          } else {
-            this.testing.message = err.message;
-            this.testing.title = "Unknown error";
-          }
-        });
-      }).finally(() => {
-        if (this.testing) {
-          this.testing.done = true;
+      return datasource.testDatasource().then(result => {
+        this.testing.message = result.message;
+        this.testing.status = result.status;
+        this.testing.title = result.title;
+      }).catch(err => {
+        if (err.statusText) {
+          this.testing.message = err.statusText;
+          this.testing.title = "HTTP Error";
+        } else {
+          this.testing.message = err.message;
+          this.testing.title = "Unknown error";
         }
       });
-    }
-
-    saveChanges() {
-      if (!this.editForm.$valid) {
-        return;
+    }).finally(() => {
+      if (this.testing) {
+        this.testing.done = true;
       }
+    });
+  }
 
-      if (this.current.id) {
-        return this.backendSrv.put('/api/datasources/' + this.current.id, this.current).then(() => {
-          this.updateFrontendSettings().then(() => {
-            this.testDatasource();
-          });
-        });
-      } else {
-        return this.backendSrv.post('/api/datasources', this.current).then(result => {
-          this.updateFrontendSettings();
-
-          datasourceCreated = true;
-          this.$location.path('datasources/edit/' + result.id);
-        });
-      }
+  saveChanges() {
+    if (!this.editForm.$valid) {
+      return;
     }
 
-    confirmDelete() {
-      this.backendSrv.delete('/api/datasources/' + this.current.id).then(() => {
-        this.$location.path('datasources');
+    if (this.current.id) {
+      return this.backendSrv.put('/api/datasources/' + this.current.id, this.current).then(() => {
+        this.updateFrontendSettings().then(() => {
+          this.testDatasource();
+        });
       });
-    }
+    } else {
+      return this.backendSrv.post('/api/datasources', this.current).then(result => {
+        this.updateFrontendSettings();
 
-    delete(s) {
-      appEvents.emit('confirm-modal', {
-        title: 'Delete',
-        text: 'Are you sure you want to delete this datasource?',
-        yesText: "Delete",
-        icon: "fa-trash",
-        onConfirm: () => {
-          this.confirmDelete();
-        }
+        datasourceCreated = true;
+        this.$location.path('datasources/edit/' + result.id);
       });
     }
+  }
+
+  confirmDelete() {
+    this.backendSrv.delete('/api/datasources/' + this.current.id).then(() => {
+      this.$location.path('datasources');
+    });
+  }
+
+  delete(s) {
+    appEvents.emit('confirm-modal', {
+      title: 'Delete',
+      text: 'Are you sure you want to delete this datasource?',
+      yesText: "Delete",
+      icon: "fa-trash",
+      onConfirm: () => {
+        this.confirmDelete();
+      }
+    });
+  }
 }
 
 coreModule.controller('DataSourceEditCtrl', DataSourceEditCtrl);

+ 26 - 16
public/app/features/plugins/ds_list_ctrl.ts

@@ -6,31 +6,41 @@ import coreModule from '../../core/core_module';
 
 export class DataSourcesCtrl {
   datasources: any;
+  navModel: any;
 
   /** @ngInject */
-  constructor(private $scope, private $location, private $http, private backendSrv, private datasourceSrv) {
-    backendSrv.get('/api/datasources')
-      .then((result) => {
-        this.datasources = result;
-      });
+  constructor(
+    private $scope,
+    private $location,
+    private $http,
+    private backendSrv,
+    private datasourceSrv,
+    private navModelSrv
+  ) {
+
+    this.navModel = this.navModelSrv.getDatasourceNav(0);
+
+    backendSrv.get('/api/datasources').then(result => {
+      this.datasources = result;
+    });
   }
 
   removeDataSourceConfirmed(ds) {
 
     this.backendSrv.delete('/api/datasources/' + ds.id)
-      .then(() => {
-        this.$scope.appEvent('alert-success', ['Datasource deleted', '']);
-      }, () => {
-        this.$scope.appEvent('alert-error', ['Unable to delete datasource', '']);
-      }).then(() => {
+    .then(() => {
+      this.$scope.appEvent('alert-success', ['Datasource deleted', '']);
+    }, () => {
+      this.$scope.appEvent('alert-error', ['Unable to delete datasource', '']);
+    }).then(() => {
       this.backendSrv.get('/api/datasources')
-        .then((result) => {
-          this.datasources = result;
-        });
+      .then((result) => {
+        this.datasources = result;
+      });
       this.backendSrv.get('/api/frontend/settings')
-        .then((settings) => {
-          this.datasourceSrv.init(settings.datasources);
-        });
+      .then((settings) => {
+        this.datasourceSrv.init(settings.datasources);
+      });
     });
   }
 

+ 1 - 2
public/app/features/plugins/partials/ds_edit.html

@@ -1,5 +1,4 @@
-<navbar title="Data Sources" title-url="datasources" icon="icon-gf icon-gf-datasources">
-</navbar>
+<navbar model="ctrl.navModel"></navbar>
 
 <div class="page-container">
 

+ 1 - 5
public/app/features/plugins/partials/ds_list.html

@@ -1,8 +1,4 @@
-<navbar
-	title="Data Sources"
-	title-url="datasources"
-	icon="icon-gf icon-gf-datasources">
-</navbar>
+<navbar model="ctrl.navModel"></navbar>
 
 <div class="page-container">
 	<div class="page-header">

+ 1 - 2
public/app/features/plugins/partials/plugin_edit.html

@@ -1,5 +1,4 @@
-<navbar title="Plugins" title-url="plugins" icon="icon-gf icon-gf-apps">
-</navbar>
+<navbar model="ctrl.navModel"></navbar>
 
 <div class="page-container" ng-init="ctrl.init()">
   <div class="page-header">

+ 1 - 2
public/app/features/plugins/partials/plugin_list.html

@@ -1,5 +1,4 @@
-<navbar title="Plugins" icon="icon-gf icon-gf-apps" title-url="plugins">
-</navbar>
+<navbar model="ctrl.navModel"></navbar>
 
 <div class="page-container">
   <div class="page-header">

+ 1 - 2
public/app/features/plugins/partials/plugin_page.html

@@ -1,5 +1,4 @@
-<navbar icon-url="{{ctrl.appLogoUrl}}" title="{{ctrl.appModel.name}}" title-url="{{ctrl.appModel.defaultNavUrl}}">
-</navbar>
+<navbar model="ctrl.navModel" ng-if="ctrl.navModel"></navbar>
 
 <div class="page-container" >
 	<div ng-if="ctrl.page">

+ 12 - 7
public/app/features/plugins/plugin_edit_ctrl.ts

@@ -13,17 +13,22 @@ export class PluginEditCtrl {
   includedDatasources: any;
   tabIndex: number;
   tabs: any;
+  navModel: any;
   hasDashboards: any;
   preUpdateHook: () => any;
   postUpdateHook: () => any;
 
   /** @ngInject */
-  constructor(private $scope,
-              private $rootScope,
-              private backendSrv,
-              private $routeParams,
-              private $sce,
-              private $http) {
+  constructor(
+    private $scope,
+    private $rootScope,
+    private backendSrv,
+    private $routeParams,
+    private $sce,
+    private $http,
+    private navModelSrv,
+  ) {
+    this.navModel = navModelSrv.getPluginsNav();
     this.model = {};
     this.pluginId = $routeParams.pluginId;
     this.tabIndex = 0;
@@ -31,7 +36,7 @@ export class PluginEditCtrl {
 
     this.preUpdateHook = () => Promise.resolve();
     this.postUpdateHook = () => Promise.resolve();
-   }
+  }
 
   init() {
     return this.backendSrv.get(`/api/plugins/${this.pluginId}/settings`).then(result => {

+ 3 - 1
public/app/features/plugins/plugin_list_ctrl.ts

@@ -5,10 +5,12 @@ import angular from 'angular';
 export class PluginListCtrl {
   plugins: any[];
   tabIndex: number;
+  navModel: any;
 
   /** @ngInject */
-  constructor(private backendSrv: any, $location) {
+  constructor(private backendSrv: any, $location, navModelSrv) {
     this.tabIndex = 0;
+    this.navModel = navModelSrv.getPluginsNav();
 
     var pluginType = $location.search().type || 'panel';
     switch (pluginType) {

+ 43 - 2
public/app/features/plugins/plugin_page_ctrl.ts

@@ -2,6 +2,7 @@
 
 import angular from 'angular';
 import _ from 'lodash';
+import {NavModel} from 'app/core/core';
 
 var pluginInfoCache = {};
 
@@ -9,7 +10,7 @@ export class AppPageCtrl {
   page: any;
   pluginId: any;
   appModel: any;
-  appLogoUrl: any;
+  navModel: NavModel;
 
   /** @ngInject */
   constructor(private backendSrv, private $routeParams: any, private $rootScope) {
@@ -25,13 +26,53 @@ export class AppPageCtrl {
   initPage(app) {
     this.appModel = app;
     this.page = _.find(app.includes, {slug: this.$routeParams.slug});
-    this.appLogoUrl = app.info.logos.small;
 
     pluginInfoCache[this.pluginId] = app;
 
     if (!this.page) {
       this.$rootScope.appEvent('alert-error', ['App Page Not Found', '']);
+
+      this.navModel = {
+        section: {
+          title: "Page not found",
+          url: app.defaultNavUrl,
+          icon: 'icon-gf icon-gf-sadface',
+        },
+        menu: [],
+      };
+
+      return;
+    }
+
+    let menu = [];
+
+    for (let item of app.includes) {
+      if (item.addToNav) {
+        if (item.type === 'dashboard') {
+          menu.push({
+            title: item.name,
+            url: 'dashboard/db/' + item.slug,
+            icon: 'fa fa-fw fa-dot-circle-o',
+          });
+        }
+        if (item.type === 'page') {
+          menu.push({
+            title: item.name,
+            url: `plugins/${app.id}/page/${item.slug}`,
+            icon: 'fa fa-fw fa-dot-circle-o',
+          });
+        }
+      }
     }
+
+    this.navModel = {
+      section: {
+        title: app.name,
+        url: app.defaultNavUrl,
+        iconUrl: app.info.logos.small,
+      },
+      menu: menu,
+    };
   }
 
   loadPluginInfo() {

+ 1 - 2
public/app/features/snapshot/partials/snapshots.html

@@ -1,5 +1,4 @@
-<navbar icon="icon-gf icon-gf-snapshot" title="Snapshots" title-url="dashboard/snapshots">
-</navbar>
+<navbar model="ctrl.navModel"></navbar>
 
 <div class="page-container">
   <div class="page-header">

+ 10 - 1
public/app/features/snapshot/snapshot_ctrl.ts

@@ -4,11 +4,20 @@ import angular from 'angular';
 import _ from 'lodash';
 
 export class SnapshotsCtrl {
-
+  navModel: any;
   snapshots: any;
 
   /** @ngInject */
   constructor(private $rootScope, private backendSrv) {
+    this.navModel = {
+      section: {
+        title: 'Snapshots',
+        icon:  'icon-gf icon-gf-snapshot',
+        url: 'dashboard/snapshots',
+      },
+      menu: [],
+    };
+
     this.backendSrv.get('/api/dashboard/snapshots').then(result => {
       this.snapshots = result;
     });

+ 1 - 2
public/app/features/styleguide/styleguide.html

@@ -1,5 +1,4 @@
-<navbar icon="fa fa-fw fa-adjust" title="Style Guide" title-url="styleguide">
-</navbar>
+<navbar model="ctrl.navModel"></navbar>
 
 <div class="page-container">
 	<div class="page-header">

+ 3 - 1
public/app/features/styleguide/styleguide.ts

@@ -12,9 +12,11 @@ class StyleGuideCtrl {
   icons: any = [];
   page: any;
   pages = ['colors', 'buttons', 'icons', 'plugins'];
+  navModel: any;
 
   /** @ngInject **/
-  constructor(private $http, private $routeParams, private $location, private backendSrv) {
+  constructor(private $http, private $routeParams, private $location, private backendSrv, navModelSrv) {
+    this.navModel = navModelSrv.getAdminNav();
     this.theme = config.bootData.user.lightTheme ? 'light': 'dark';
     this.page = {};
 

+ 1 - 0
public/app/headers/common.d.ts

@@ -72,3 +72,4 @@ declare module 'd3' {
   var d3: any;
   export default d3;
 }
+

+ 2 - 3
public/app/partials/dashboard.html

@@ -1,9 +1,8 @@
 <div dash-class ng-if="dashboard">
-	<dashnav></dashnav>
+	<dashnav dashboard="dashboard"></dashnav>
 
 	<div class="dashboard-container">
-		<div dash-editor-view></div>
-		<dashboard-search></dashboard-search>
+		<div dash-editor-view class="dash-edit-view"></div>
 		<div class="clearfix"></div>
 
 		<dashboard-submenu ng-if="dashboard.meta.submenuEnabled" dashboard="dashboard"></dashboard-submenu>

+ 1 - 2
public/app/partials/error.html

@@ -1,5 +1,4 @@
-<navbar title="404" icon="fa fa-fw fa-question" title-url="/">
-</navbar>
+<navbar model="navModel"></navbar>
 
 <div class="page-container">
 

+ 6 - 2
public/app/plugins/datasource/elasticsearch/query_builder.js

@@ -119,8 +119,12 @@ function (queryDef) {
       query.fields = ["*", "_source"];
     }
 
-    query.script_fields = {},
-    query.docvalue_fields = [this.timeField];
+    query.script_fields = {};
+    if (this.esVersion < 5) {
+      query.fielddata_fields = [this.timeField];
+    } else {
+      query.docvalue_fields = [this.timeField];
+    }
     return query;
   };
 

+ 23 - 3
public/app/plugins/datasource/grafana/datasource.ts

@@ -8,11 +8,31 @@ class GrafanaDatasource {
   constructor(private backendSrv, private $q) {}
 
   query(options) {
-    return this.$q.when({data: []});
+    return this.backendSrv.get('/api/tsdb/testdata/random-walk', {
+      from: options.range.from.valueOf(),
+      to: options.range.to.valueOf(),
+      intervalMs: options.intervalMs,
+      maxDataPoints: options.maxDataPoints,
+    }).then(res => {
+      var data = [];
+
+      if (res.results) {
+        _.forEach(res.results, queryRes => {
+          for (let series of queryRes.series) {
+            data.push({
+              target: series.name,
+              datapoints: series.points
+            });
+          }
+        });
+      }
+
+      return {data: data};
+    });
   }
 
-  metricFindQuery() {
-    return this.$q.when([]);
+  metricFindQuery(options) {
+    return this.$q.when({data: []});
   }
 
   annotationQuery(options) {

+ 5 - 1
public/app/plugins/datasource/grafana/partials/query.editor.html

@@ -1,7 +1,11 @@
 <query-editor-row query-ctrl="ctrl" can-collapse="false">
 	<div class="gf-form-inline">
 		<div class="gf-form">
-			<label class="gf-form-label">Test metric (fake data source)</label>
+			<label class="gf-form-label">Test data: random walk</label>
+		</div>
+
+		<div class="gf-form gf-form--grow">
+			<div class="gf-form-label gf-form-label--grow"></div>
 		</div>
 	</div>
 </query-editor-row>

Некоторые файлы не были показаны из-за большого количества измененных файлов