Bladeren bron

Merge branch 'master' of https://github.com/grafana/grafana into metadata

utkarshcmu 10 jaren geleden
bovenliggende
commit
338afc80d5
100 gewijzigde bestanden met toevoegingen van 1341 en 741 verwijderingen
  1. 1 0
      .gitignore
  2. 4 3
      CHANGELOG.md
  3. 2 2
      docs/sources/reference/playlist.md
  4. 2 1
      package.json
  5. 9 2
      pkg/api/app_routes.go
  6. 1 0
      pkg/api/dataproxy.go
  7. 2 0
      pkg/api/frontendsettings.go
  8. 3 39
      pkg/api/playlist.go
  9. 88 0
      pkg/api/playlist_play.go
  10. 0 1
      pkg/cmd/web.go
  11. 5 5
      pkg/log/console.go
  12. 2 2
      pkg/log/file.go
  13. 28 9
      pkg/log/log.go
  14. 1 1
      pkg/log/syslog.go
  15. 24 9
      pkg/models/app_settings.go
  16. 5 0
      pkg/models/dashboards.go
  17. 0 9
      pkg/models/playlist.go
  18. 18 7
      pkg/services/sqlstore/app_settings.go
  19. 18 0
      pkg/services/sqlstore/dashboard.go
  20. 6 3
      pkg/services/sqlstore/migrations/app_settings.go
  21. 0 18
      pkg/services/sqlstore/playlist.go
  22. 44 0
      pkg/services/sqlstore/playlist_test.go
  23. 2 2
      pkg/services/sqlstore/stats.go
  24. 66 0
      pkg/util/encryption.go
  25. 27 0
      pkg/util/encryption_test.go
  26. 5 0
      pkg/util/url.go
  27. 46 0
      pkg/util/url_test.go
  28. 11 2
      public/app/core/components/grafana_app.ts
  29. 3 2
      public/app/core/components/navbar/navbar.html
  30. 1 1
      public/app/core/components/navbar/navbar.ts
  31. 1 1
      public/app/core/components/search/search.ts
  32. 6 0
      public/app/core/components/sidemenu/sidemenu.html
  33. 13 9
      public/app/core/components/sidemenu/sidemenu.ts
  34. 0 1
      public/app/core/core.ts
  35. 3 3
      public/app/core/directives/dash_edit_link.js
  36. 0 50
      public/app/core/directives/topnav.js
  37. 1 1
      public/app/core/directives/value_select_dropdown.js
  38. 6 3
      public/app/core/filters/filters.ts
  39. 32 32
      public/app/core/routes/all.js
  40. 1 1
      public/app/core/services/alert_srv.js
  41. 13 0
      public/app/core/services/context_srv.js
  42. 3 4
      public/app/features/admin/partials/edit_org.html
  43. 2 4
      public/app/features/admin/partials/edit_user.html
  44. 4 5
      public/app/features/admin/partials/new_user.html
  45. 2 5
      public/app/features/admin/partials/orgs.html
  46. 2 2
      public/app/features/admin/partials/settings.html
  47. 2 5
      public/app/features/admin/partials/stats.html
  48. 8 9
      public/app/features/admin/partials/users.html
  49. 1 1
      public/app/features/annotations/editor_ctrl.js
  50. 1 1
      public/app/features/annotations/partials/editor.html
  51. 1 1
      public/app/features/annotations/query_editor.ts
  52. 1 0
      public/app/features/apps/edit_ctrl.ts
  53. 2 5
      public/app/features/apps/partials/list.html
  54. 1 1
      public/app/features/dashboard/dashboardCtrl.js
  55. 15 3
      public/app/features/dashboard/dashnav/dashnav.html
  56. 8 5
      public/app/features/dashboard/dashnav/dashnav.ts
  57. 1 1
      public/app/features/dashboard/keybindings.js
  58. 3 3
      public/app/features/dashboard/partials/import.html
  59. 1 1
      public/app/features/dashboard/partials/settings.html
  60. 2 2
      public/app/features/dashboard/partials/shareModal.html
  61. 0 29
      public/app/features/dashboard/rowCtrl.js
  62. 2 2
      public/app/features/dashboard/shareModalCtrl.js
  63. 1 1
      public/app/features/dashboard/submenu/submenu.ts
  64. 0 47
      public/app/features/dashboard/timepicker/custom.html
  65. 3 3
      public/app/features/dashboard/timepicker/dropdown.html
  66. 14 1
      public/app/features/dashboard/timepicker/input_date.ts
  67. 3 3
      public/app/features/dashboard/timepicker/timepicker.ts
  68. 1 1
      public/app/features/dashboard/unsavedChangesSrv.js
  69. 30 15
      public/app/features/dashboard/viewStateSrv.js
  70. 1 1
      public/app/features/dashlinks/module.js
  71. 1 1
      public/app/features/datasources/edit_ctrl.js
  72. 2 2
      public/app/features/datasources/partials/edit.html
  73. 3 3
      public/app/features/datasources/partials/list.html
  74. 1 1
      public/app/features/org/orgApiKeysCtrl.js
  75. 1 1
      public/app/features/org/orgUsersCtrl.js
  76. 2 2
      public/app/features/org/partials/newOrg.html
  77. 2 2
      public/app/features/org/partials/orgApiKeys.html
  78. 2 2
      public/app/features/org/partials/orgDetails.html
  79. 2 2
      public/app/features/org/partials/orgUsers.html
  80. 1 2
      public/app/features/panel/all.js
  81. 234 0
      public/app/features/panel/metrics_panel_ctrl.ts
  82. 51 0
      public/app/features/panel/panel.ts
  83. 179 0
      public/app/features/panel/panel_ctrl.ts
  84. 17 15
      public/app/features/panel/panel_directive.js
  85. 28 0
      public/app/features/panel/panel_editor_tab.ts
  86. 74 16
      public/app/features/panel/panel_loader.ts
  87. 23 21
      public/app/features/panel/panel_menu.js
  88. 0 7
      public/app/features/panel/panel_meta.js
  89. 0 48
      public/app/features/panel/panel_meta2.ts
  90. 0 162
      public/app/features/panel/panel_srv.js
  91. 12 12
      public/app/features/panel/partials/panel.html
  92. 7 7
      public/app/features/panel/partials/panelTime.html
  93. 2 1
      public/app/features/panel/partials/soloPanel.html
  94. 4 4
      public/app/features/panel/query_editor.ts
  95. 1 6
      public/app/features/panel/solo_panel_ctrl.js
  96. 1 1
      public/app/features/panellinks/module.js
  97. 1 0
      public/app/features/playlist/all.js
  98. 47 41
      public/app/features/playlist/partials/playlist.html
  99. 26 0
      public/app/features/playlist/partials/playlist_search.html
  100. 2 2
      public/app/features/playlist/partials/playlists.html

+ 1 - 0
.gitignore

@@ -6,6 +6,7 @@ awsconfig
 /dist
 /dist
 /emails/dist
 /emails/dist
 /public_gen
 /public_gen
+/public/vendor/npm
 /tmp
 /tmp
 vendor/phantomjs/phantomjs
 vendor/phantomjs/phantomjs
 
 

+ 4 - 3
CHANGELOG.md

@@ -6,14 +6,15 @@
 * **InfluxDB**: Support for policy selection in query editor, closes [#2018](https://github.com/grafana/grafana/issues/2018)
 * **InfluxDB**: Support for policy selection in query editor, closes [#2018](https://github.com/grafana/grafana/issues/2018)
 
 
 ### Breaking changes
 ### Breaking changes
-**Plugin API**: Both datasource and panel plugin api (and plugin.json schema) as been updated, requiring a minor update to plugins. See [plugin api](https://github.com/grafana/grafana/blob/master/public/app/plugins/plugin_api.md) for more info.
-**InfluxDB 0.8.x** The data source for the old version of influxdb (0.8.x) is no longer included in default builds. Can easily be installed via improved plugin system, closes #3523
-**KairosDB** The data source is no longer included in default builds. Can easily be installed via improved plugin system, closes #3524
+* **Plugin API**: Both datasource and panel plugin api (and plugin.json schema) have been updated, requiring a minor update to plugins. See [plugin api](https://github.com/grafana/grafana/blob/master/public/app/plugins/plugin_api.md) for more info.
+* **InfluxDB 0.8.x** The data source for the old version of influxdb (0.8.x) is no longer included in default builds, but can easily be installed via improved plugin system, closes [#3523](https://github.com/grafana/grafana/issues/3523)
+* **KairosDB** The data source is no longer included in default builds, but can easily be installed via improved plugin system, closes [#3524](https://github.com/grafana/grafana/issues/3524)
 
 
 ### Enhancements
 ### Enhancements
 * **Sessions**: Support for memcached as session storage, closes [#3458](https://github.com/grafana/grafana/pull/3458)
 * **Sessions**: Support for memcached as session storage, closes [#3458](https://github.com/grafana/grafana/pull/3458)
 * **mysql**: Grafana now supports ssl for mysql, closes [#3584](https://github.com/grafana/grafana/pull/3584)
 * **mysql**: Grafana now supports ssl for mysql, closes [#3584](https://github.com/grafana/grafana/pull/3584)
 * **snapshot**: Annotations are now included in snapshots, closes [#3635](https://github.com/grafana/grafana/pull/3635)
 * **snapshot**: Annotations are now included in snapshots, closes [#3635](https://github.com/grafana/grafana/pull/3635)
+* **Admin**: Admin can now have global overview of Grafana setup, closes [#3812](https://github.com/grafana/grafana/issues/3812)
 
 
 ### Bug fixes
 ### Bug fixes
 * **Playlist**: Fix for memory leak when running a playlist, closes [#3794](https://github.com/grafana/grafana/pull/3794)
 * **Playlist**: Fix for memory leak when running a playlist, closes [#3794](https://github.com/grafana/grafana/pull/3794)

+ 2 - 2
docs/sources/reference/playlist.md

@@ -18,11 +18,11 @@ The Playlist feature can be accessed from Grafana's sidemenu. Click the 'Playlis
 
 
 Click on "New Playlist" button to create a new playlist. Firstly, name your playlist and configure a time interval for Grafana to wait on a particular Dashboard before advancing to the next one on the Playlist.
 Click on "New Playlist" button to create a new playlist. Firstly, name your playlist and configure a time interval for Grafana to wait on a particular Dashboard before advancing to the next one on the Playlist.
 
 
-You can search Dashboards by name (or use a regular expression), and add them to your Playlist. By default, your starred dashboards will appear as candidates for the Playlist.
+You can search Dashboards by name (or use a regular expression), and add them to your Playlist. Or you could add tags which will include all the dashboards that belongs to a tag when the playlist start playing. By default, your starred dashboards will appear as candidates for the Playlist.
 
 
 Be sure to click the "Add to dashboard" button next to the Dashboard name to add it to the Playlist. To remove a dashboard from the playlist click on "Remove[x]" button from the playlist.
 Be sure to click the "Add to dashboard" button next to the Dashboard name to add it to the Playlist. To remove a dashboard from the playlist click on "Remove[x]" button from the playlist.
 
 
-Since the Playlist is basically a list of Dashboards, ensure that all the Dashboards you want to appear in your Playlist are added here. 
+Since the Playlist is basically a list of Dashboards, ensure that all the Dashboards you want to appear in your Playlist are added here.
 
 
 ## Saving the playlist
 ## Saving the playlist
 
 

+ 2 - 1
package.json

@@ -59,7 +59,8 @@
   },
   },
   "scripts": {
   "scripts": {
     "test": "grunt test",
     "test": "grunt test",
-    "coveralls": "grunt karma:coveralls && rm -rf ./coverage"
+    "coveralls": "grunt karma:coveralls && rm -rf ./coverage",
+    "postinstall": "grunt copy:node_modules"
   },
   },
   "license": "Apache-2.0",
   "license": "Apache-2.0",
   "dependencies": {
   "dependencies": {

+ 9 - 2
pkg/api/app_routes.go

@@ -94,8 +94,15 @@ func NewApiPluginProxy(ctx *middleware.Context, proxyPath string, route *plugins
 				ctx.JsonApiErr(500, "failed to get AppSettings.", err)
 				ctx.JsonApiErr(500, "failed to get AppSettings.", err)
 				return
 				return
 			}
 			}
-
-			err = t.Execute(&contentBuf, query.Result.JsonData)
+			type templateData struct {
+				JsonData       map[string]interface{}
+				SecureJsonData map[string]string
+			}
+			data := templateData{
+				JsonData:       query.Result.JsonData,
+				SecureJsonData: query.Result.SecureJsonData.Decrypt(),
+			}
+			err = t.Execute(&contentBuf, data)
 			if err != nil {
 			if err != nil {
 				ctx.JsonApiErr(500, fmt.Sprintf("failed to execute header content template for header %s.", header.Name), err)
 				ctx.JsonApiErr(500, fmt.Sprintf("failed to execute header content template for header %s.", header.Name), err)
 				return
 				return

+ 1 - 0
pkg/api/dataproxy.go

@@ -103,5 +103,6 @@ func ProxyDataSourceRequest(c *middleware.Context) {
 		proxy := NewReverseProxy(ds, proxyPath, targetUrl)
 		proxy := NewReverseProxy(ds, proxyPath, targetUrl)
 		proxy.Transport = dataProxyTransport
 		proxy.Transport = dataProxyTransport
 		proxy.ServeHTTP(c.Resp, c.Req.Request)
 		proxy.ServeHTTP(c.Resp, c.Req.Request)
+		c.Resp.Header().Del("Set-Cookie")
 	}
 	}
 }
 }

+ 2 - 0
pkg/api/frontendsettings.go

@@ -123,6 +123,8 @@ func getFrontendSettingsMap(c *middleware.Context) (map[string]interface{}, erro
 		panels[panel.Id] = map[string]interface{}{
 		panels[panel.Id] = map[string]interface{}{
 			"module": panel.Module,
 			"module": panel.Module,
 			"name":   panel.Name,
 			"name":   panel.Name,
+			"id":     panel.Id,
+			"info":   panel.Info,
 		}
 		}
 	}
 	}
 
 

+ 3 - 39
pkg/api/playlist.go

@@ -1,11 +1,8 @@
 package api
 package api
 
 
 import (
 import (
-	"errors"
-	"strconv"
-
 	"github.com/grafana/grafana/pkg/bus"
 	"github.com/grafana/grafana/pkg/bus"
-	"github.com/grafana/grafana/pkg/log"
+	_ "github.com/grafana/grafana/pkg/log"
 	"github.com/grafana/grafana/pkg/middleware"
 	"github.com/grafana/grafana/pkg/middleware"
 	m "github.com/grafana/grafana/pkg/models"
 	m "github.com/grafana/grafana/pkg/models"
 )
 )
@@ -101,39 +98,6 @@ func LoadPlaylistItems(id int64) ([]m.PlaylistItem, error) {
 	return *itemQuery.Result, nil
 	return *itemQuery.Result, nil
 }
 }
 
 
-func LoadPlaylistDashboards(id int64) ([]m.PlaylistDashboardDto, error) {
-	playlistItems, _ := LoadPlaylistItems(id)
-
-	dashboardIds := make([]int64, 0)
-
-	for _, i := range playlistItems {
-		dashboardId, _ := strconv.ParseInt(i.Value, 10, 64)
-		dashboardIds = append(dashboardIds, dashboardId)
-	}
-
-	if len(dashboardIds) == 0 {
-		return make([]m.PlaylistDashboardDto, 0), nil
-	}
-
-	dashboardQuery := m.GetPlaylistDashboardsQuery{DashboardIds: dashboardIds}
-	if err := bus.Dispatch(&dashboardQuery); err != nil {
-		log.Warn("dashboardquery failed: %v", err)
-		return nil, errors.New("Playlist not found")
-	}
-
-	dtos := make([]m.PlaylistDashboardDto, 0)
-	for _, item := range *dashboardQuery.Result {
-		dtos = append(dtos, m.PlaylistDashboardDto{
-			Id:    item.Id,
-			Slug:  item.Slug,
-			Title: item.Title,
-			Uri:   "db/" + item.Slug,
-		})
-	}
-
-	return dtos, nil
-}
-
 func GetPlaylistItems(c *middleware.Context) Response {
 func GetPlaylistItems(c *middleware.Context) Response {
 	id := c.ParamsInt64(":id")
 	id := c.ParamsInt64(":id")
 
 
@@ -147,9 +111,9 @@ func GetPlaylistItems(c *middleware.Context) Response {
 }
 }
 
 
 func GetPlaylistDashboards(c *middleware.Context) Response {
 func GetPlaylistDashboards(c *middleware.Context) Response {
-	id := c.ParamsInt64(":id")
+	playlistId := c.ParamsInt64(":id")
 
 
-	playlists, err := LoadPlaylistDashboards(id)
+	playlists, err := LoadPlaylistDashboards(c.OrgId, c.UserId, playlistId)
 	if err != nil {
 	if err != nil {
 		return ApiError(500, "Could not load dashboards", err)
 		return ApiError(500, "Could not load dashboards", err)
 	}
 	}

+ 88 - 0
pkg/api/playlist_play.go

@@ -0,0 +1,88 @@
+package api
+
+import (
+	"errors"
+	"strconv"
+
+	"github.com/grafana/grafana/pkg/bus"
+	_ "github.com/grafana/grafana/pkg/log"
+	m "github.com/grafana/grafana/pkg/models"
+	"github.com/grafana/grafana/pkg/services/search"
+)
+
+func populateDashboardsById(dashboardByIds []int64) ([]m.PlaylistDashboardDto, error) {
+	result := make([]m.PlaylistDashboardDto, 0)
+
+	if len(dashboardByIds) > 0 {
+		dashboardQuery := m.GetDashboardsQuery{DashboardIds: dashboardByIds}
+		if err := bus.Dispatch(&dashboardQuery); err != nil {
+			return result, errors.New("Playlist not found") //TODO: dont swallow error
+		}
+
+		for _, item := range *dashboardQuery.Result {
+			result = append(result, m.PlaylistDashboardDto{
+				Id:    item.Id,
+				Slug:  item.Slug,
+				Title: item.Title,
+				Uri:   "db/" + item.Slug,
+			})
+		}
+	}
+
+	return result, nil
+}
+
+func populateDashboardsByTag(orgId, userId int64, dashboardByTag []string) []m.PlaylistDashboardDto {
+	result := make([]m.PlaylistDashboardDto, 0)
+
+	if len(dashboardByTag) > 0 {
+		for _, tag := range dashboardByTag {
+			searchQuery := search.Query{
+				Title:     "",
+				Tags:      []string{tag},
+				UserId:    userId,
+				Limit:     100,
+				IsStarred: false,
+				OrgId:     orgId,
+			}
+
+			if err := bus.Dispatch(&searchQuery); err == nil {
+				for _, item := range searchQuery.Result {
+					result = append(result, m.PlaylistDashboardDto{
+						Id:    item.Id,
+						Title: item.Title,
+						Uri:   item.Uri,
+					})
+				}
+			}
+		}
+	}
+
+	return result
+}
+
+func LoadPlaylistDashboards(orgId, userId, playlistId int64) ([]m.PlaylistDashboardDto, error) {
+	playlistItems, _ := LoadPlaylistItems(playlistId)
+
+	dashboardByIds := make([]int64, 0)
+	dashboardByTag := make([]string, 0)
+
+	for _, i := range playlistItems {
+		if i.Type == "dashboard_by_id" {
+			dashboardId, _ := strconv.ParseInt(i.Value, 10, 64)
+			dashboardByIds = append(dashboardByIds, dashboardId)
+		}
+
+		if i.Type == "dashboard_by_tag" {
+			dashboardByTag = append(dashboardByTag, i.Value)
+		}
+	}
+
+	result := make([]m.PlaylistDashboardDto, 0)
+
+	var k, _ = populateDashboardsById(dashboardByIds)
+	result = append(result, k...)
+	result = append(result, populateDashboardsByTag(orgId, userId, dashboardByTag)...)
+
+	return result, nil
+}

+ 0 - 1
pkg/cmd/web.go

@@ -36,7 +36,6 @@ func newMacaron() *macaron.Macaron {
 	}
 	}
 
 
 	mapStatic(m, setting.StaticRootPath, "", "public")
 	mapStatic(m, setting.StaticRootPath, "", "public")
-	mapStatic(m, setting.StaticRootPath, "app", "app")
 	mapStatic(m, setting.StaticRootPath, "css", "css")
 	mapStatic(m, setting.StaticRootPath, "css", "css")
 	mapStatic(m, setting.StaticRootPath, "img", "img")
 	mapStatic(m, setting.StaticRootPath, "img", "img")
 	mapStatic(m, setting.StaticRootPath, "fonts", "fonts")
 	mapStatic(m, setting.StaticRootPath, "fonts", "fonts")

+ 5 - 5
pkg/log/console.go

@@ -46,8 +46,8 @@ var (
 // ConsoleWriter implements LoggerInterface and writes messages to terminal.
 // ConsoleWriter implements LoggerInterface and writes messages to terminal.
 type ConsoleWriter struct {
 type ConsoleWriter struct {
 	lg         *log.Logger
 	lg         *log.Logger
-	Level      int  `json:"level"`
-	Formatting bool `json:"formatting"`
+	Level      LogLevel `json:"level"`
+	Formatting bool     `json:"formatting"`
 }
 }
 
 
 // create ConsoleWriter returning as LoggerInterface.
 // create ConsoleWriter returning as LoggerInterface.
@@ -63,7 +63,7 @@ func (cw *ConsoleWriter) Init(config string) error {
 	return json.Unmarshal([]byte(config), cw)
 	return json.Unmarshal([]byte(config), cw)
 }
 }
 
 
-func (cw *ConsoleWriter) WriteMsg(msg string, skip, level int) error {
+func (cw *ConsoleWriter) WriteMsg(msg string, skip int, level LogLevel) error {
 	if cw.Level > level {
 	if cw.Level > level {
 		return nil
 		return nil
 	}
 	}
@@ -82,11 +82,11 @@ func (_ *ConsoleWriter) Flush() {
 func (_ *ConsoleWriter) Destroy() {
 func (_ *ConsoleWriter) Destroy() {
 }
 }
 
 
-func printConsole(level int, msg string) {
+func printConsole(level LogLevel, msg string) {
 	consoleWriter.WriteMsg(msg, 0, level)
 	consoleWriter.WriteMsg(msg, 0, level)
 }
 }
 
 
-func printfConsole(level int, format string, v ...interface{}) {
+func printfConsole(level LogLevel, format string, v ...interface{}) {
 	consoleWriter.WriteMsg(fmt.Sprintf(format, v...), 0, level)
 	consoleWriter.WriteMsg(fmt.Sprintf(format, v...), 0, level)
 }
 }
 
 

+ 2 - 2
pkg/log/file.go

@@ -41,7 +41,7 @@ type FileLogWriter struct {
 
 
 	startLock sync.Mutex // Only one log can write to the file
 	startLock sync.Mutex // Only one log can write to the file
 
 
-	Level int `json:"level"`
+	Level LogLevel `json:"level"`
 }
 }
 
 
 // an *os.File writer with locker.
 // an *os.File writer with locker.
@@ -132,7 +132,7 @@ func (w *FileLogWriter) docheck(size int) {
 }
 }
 
 
 // write logger message into file.
 // write logger message into file.
-func (w *FileLogWriter) WriteMsg(msg string, skip, level int) error {
+func (w *FileLogWriter) WriteMsg(msg string, skip int, level LogLevel) error {
 	if level < w.Level {
 	if level < w.Level {
 		return nil
 		return nil
 	}
 	}

+ 28 - 9
pkg/log/log.go

@@ -99,7 +99,7 @@ func Close() {
 type LogLevel int
 type LogLevel int
 
 
 const (
 const (
-	TRACE = iota
+	TRACE LogLevel = iota
 	DEBUG
 	DEBUG
 	INFO
 	INFO
 	WARN
 	WARN
@@ -111,7 +111,7 @@ const (
 // LoggerInterface represents behaviors of a logger provider.
 // LoggerInterface represents behaviors of a logger provider.
 type LoggerInterface interface {
 type LoggerInterface interface {
 	Init(config string) error
 	Init(config string) error
-	WriteMsg(msg string, skip, level int) error
+	WriteMsg(msg string, skip int, level LogLevel) error
 	Destroy()
 	Destroy()
 	Flush()
 	Flush()
 }
 }
@@ -132,8 +132,9 @@ func Register(name string, log loggerType) {
 }
 }
 
 
 type logMsg struct {
 type logMsg struct {
-	skip, level int
-	msg         string
+	skip  int
+	level LogLevel
+	msg   string
 }
 }
 
 
 // Logger is default logger in beego application.
 // Logger is default logger in beego application.
@@ -141,7 +142,7 @@ type logMsg struct {
 type Logger struct {
 type Logger struct {
 	adapter string
 	adapter string
 	lock    sync.Mutex
 	lock    sync.Mutex
-	level   int
+	level   LogLevel
 	msg     chan *logMsg
 	msg     chan *logMsg
 	outputs map[string]LoggerInterface
 	outputs map[string]LoggerInterface
 	quit    chan bool
 	quit    chan bool
@@ -188,10 +189,7 @@ func (l *Logger) DelLogger(adapter string) error {
 	return nil
 	return nil
 }
 }
 
 
-func (l *Logger) writerMsg(skip, level int, msg string) error {
-	if l.level > level {
-		return nil
-	}
+func (l *Logger) writerMsg(skip int, level LogLevel, msg string) error {
 	lm := &logMsg{
 	lm := &logMsg{
 		skip:  skip,
 		skip:  skip,
 		level: level,
 		level: level,
@@ -266,36 +264,57 @@ func (l *Logger) Close() {
 }
 }
 
 
 func (l *Logger) Trace(format string, v ...interface{}) {
 func (l *Logger) Trace(format string, v ...interface{}) {
+	if l.level > TRACE {
+		return
+	}
 	msg := fmt.Sprintf("[T] "+format, v...)
 	msg := fmt.Sprintf("[T] "+format, v...)
 	l.writerMsg(0, TRACE, msg)
 	l.writerMsg(0, TRACE, msg)
 }
 }
 
 
 func (l *Logger) Debug(format string, v ...interface{}) {
 func (l *Logger) Debug(format string, v ...interface{}) {
+	if l.level > DEBUG {
+		return
+	}
 	msg := fmt.Sprintf("[D] "+format, v...)
 	msg := fmt.Sprintf("[D] "+format, v...)
 	l.writerMsg(0, DEBUG, msg)
 	l.writerMsg(0, DEBUG, msg)
 }
 }
 
 
 func (l *Logger) Info(format string, v ...interface{}) {
 func (l *Logger) Info(format string, v ...interface{}) {
+	if l.level > INFO {
+		return
+	}
 	msg := fmt.Sprintf("[I] "+format, v...)
 	msg := fmt.Sprintf("[I] "+format, v...)
 	l.writerMsg(0, INFO, msg)
 	l.writerMsg(0, INFO, msg)
 }
 }
 
 
 func (l *Logger) Warn(format string, v ...interface{}) {
 func (l *Logger) Warn(format string, v ...interface{}) {
+	if l.level > WARN {
+		return
+	}
 	msg := fmt.Sprintf("[W] "+format, v...)
 	msg := fmt.Sprintf("[W] "+format, v...)
 	l.writerMsg(0, WARN, msg)
 	l.writerMsg(0, WARN, msg)
 }
 }
 
 
 func (l *Logger) Error(skip int, format string, v ...interface{}) {
 func (l *Logger) Error(skip int, format string, v ...interface{}) {
+	if l.level > ERROR {
+		return
+	}
 	msg := fmt.Sprintf("[E] "+format, v...)
 	msg := fmt.Sprintf("[E] "+format, v...)
 	l.writerMsg(skip, ERROR, msg)
 	l.writerMsg(skip, ERROR, msg)
 }
 }
 
 
 func (l *Logger) Critical(skip int, format string, v ...interface{}) {
 func (l *Logger) Critical(skip int, format string, v ...interface{}) {
+	if l.level > CRITICAL {
+		return
+	}
 	msg := fmt.Sprintf("[C] "+format, v...)
 	msg := fmt.Sprintf("[C] "+format, v...)
 	l.writerMsg(skip, CRITICAL, msg)
 	l.writerMsg(skip, CRITICAL, msg)
 }
 }
 
 
 func (l *Logger) Fatal(skip int, format string, v ...interface{}) {
 func (l *Logger) Fatal(skip int, format string, v ...interface{}) {
+	if l.level > FATAL {
+		return
+	}
 	msg := fmt.Sprintf("[F] "+format, v...)
 	msg := fmt.Sprintf("[F] "+format, v...)
 	l.writerMsg(skip, FATAL, msg)
 	l.writerMsg(skip, FATAL, msg)
 	l.Close()
 	l.Close()

+ 1 - 1
pkg/log/syslog.go

@@ -39,7 +39,7 @@ func (sw *SyslogWriter) Init(config string) error {
 	return nil
 	return nil
 }
 }
 
 
-func (sw *SyslogWriter) WriteMsg(msg string, skip, level int) error {
+func (sw *SyslogWriter) WriteMsg(msg string, skip int, level LogLevel) error {
 	var err error
 	var err error
 
 
 	switch level {
 	switch level {

+ 24 - 9
pkg/models/app_settings.go

@@ -3,6 +3,9 @@ package models
 import (
 import (
 	"errors"
 	"errors"
 	"time"
 	"time"
+
+	"github.com/grafana/grafana/pkg/setting"
+	"github.com/grafana/grafana/pkg/util"
 )
 )
 
 
 var (
 var (
@@ -10,25 +13,37 @@ var (
 )
 )
 
 
 type AppSettings struct {
 type AppSettings struct {
-	Id       int64
-	AppId    string
-	OrgId    int64
-	Enabled  bool
-	Pinned   bool
-	JsonData map[string]interface{}
+	Id             int64
+	AppId          string
+	OrgId          int64
+	Enabled        bool
+	Pinned         bool
+	JsonData       map[string]interface{}
+	SecureJsonData SecureJsonData
 
 
 	Created time.Time
 	Created time.Time
 	Updated time.Time
 	Updated time.Time
 }
 }
 
 
+type SecureJsonData map[string][]byte
+
+func (s SecureJsonData) Decrypt() map[string]string {
+	decrypted := make(map[string]string)
+	for key, data := range s {
+		decrypted[key] = string(util.Decrypt(data, setting.SecretKey))
+	}
+	return decrypted
+}
+
 // ----------------------
 // ----------------------
 // COMMANDS
 // COMMANDS
 
 
 // Also acts as api DTO
 // Also acts as api DTO
 type UpdateAppSettingsCmd struct {
 type UpdateAppSettingsCmd struct {
-	Enabled  bool                   `json:"enabled"`
-	Pinned   bool                   `json:"pinned"`
-	JsonData map[string]interface{} `json:"jsonData"`
+	Enabled        bool                   `json:"enabled"`
+	Pinned         bool                   `json:"pinned"`
+	JsonData       map[string]interface{} `json:"jsonData"`
+	SecureJsonData map[string]string      `json:"secureJsonData"`
 
 
 	AppId string `json:"-"`
 	AppId string `json:"-"`
 	OrgId int64  `json:"-"`
 	OrgId int64  `json:"-"`

+ 5 - 0
pkg/models/dashboards.go

@@ -150,3 +150,8 @@ type GetDashboardTagsQuery struct {
 	OrgId  int64
 	OrgId  int64
 	Result []*DashboardTagCloudItem
 	Result []*DashboardTagCloudItem
 }
 }
+
+type GetDashboardsQuery struct {
+	DashboardIds []int64
+	Result       *[]Dashboard
+}

+ 0 - 9
pkg/models/playlist.go

@@ -76,9 +76,7 @@ type UpdatePlaylistCommand struct {
 	OrgId    int64             `json:"-"`
 	OrgId    int64             `json:"-"`
 	Id       int64             `json:"id" binding:"Required"`
 	Id       int64             `json:"id" binding:"Required"`
 	Name     string            `json:"name" binding:"Required"`
 	Name     string            `json:"name" binding:"Required"`
-	Type     string            `json:"type"`
 	Interval string            `json:"interval"`
 	Interval string            `json:"interval"`
-	Data     []int64           `json:"data"`
 	Items    []PlaylistItemDTO `json:"items"`
 	Items    []PlaylistItemDTO `json:"items"`
 
 
 	Result *PlaylistDTO
 	Result *PlaylistDTO
@@ -86,9 +84,7 @@ type UpdatePlaylistCommand struct {
 
 
 type CreatePlaylistCommand struct {
 type CreatePlaylistCommand struct {
 	Name     string            `json:"name" binding:"Required"`
 	Name     string            `json:"name" binding:"Required"`
-	Type     string            `json:"type"`
 	Interval string            `json:"interval"`
 	Interval string            `json:"interval"`
-	Data     []int64           `json:"data"`
 	Items    []PlaylistItemDTO `json:"items"`
 	Items    []PlaylistItemDTO `json:"items"`
 
 
 	OrgId  int64 `json:"-"`
 	OrgId  int64 `json:"-"`
@@ -121,8 +117,3 @@ type GetPlaylistItemsByIdQuery struct {
 	PlaylistId int64
 	PlaylistId int64
 	Result     *[]PlaylistItem
 	Result     *[]PlaylistItem
 }
 }
-
-type GetPlaylistDashboardsQuery struct {
-	DashboardIds []int64
-	Result       *PlaylistDashboards
-}

+ 18 - 7
pkg/services/sqlstore/app_settings.go

@@ -5,6 +5,8 @@ import (
 
 
 	"github.com/grafana/grafana/pkg/bus"
 	"github.com/grafana/grafana/pkg/bus"
 	m "github.com/grafana/grafana/pkg/models"
 	m "github.com/grafana/grafana/pkg/models"
+	"github.com/grafana/grafana/pkg/setting"
+	"github.com/grafana/grafana/pkg/util"
 )
 )
 
 
 func init() {
 func init() {
@@ -40,18 +42,27 @@ func UpdateAppSettings(cmd *m.UpdateAppSettingsCmd) error {
 		sess.UseBool("enabled")
 		sess.UseBool("enabled")
 		sess.UseBool("pinned")
 		sess.UseBool("pinned")
 		if !exists {
 		if !exists {
+			// encrypt secureJsonData
+			secureJsonData := make(map[string][]byte)
+			for key, data := range cmd.SecureJsonData {
+				secureJsonData[key] = util.Encrypt([]byte(data), setting.SecretKey)
+			}
 			app = m.AppSettings{
 			app = m.AppSettings{
-				AppId:    cmd.AppId,
-				OrgId:    cmd.OrgId,
-				Enabled:  cmd.Enabled,
-				Pinned:   cmd.Pinned,
-				JsonData: cmd.JsonData,
-				Created:  time.Now(),
-				Updated:  time.Now(),
+				AppId:          cmd.AppId,
+				OrgId:          cmd.OrgId,
+				Enabled:        cmd.Enabled,
+				Pinned:         cmd.Pinned,
+				JsonData:       cmd.JsonData,
+				SecureJsonData: secureJsonData,
+				Created:        time.Now(),
+				Updated:        time.Now(),
 			}
 			}
 			_, err = sess.Insert(&app)
 			_, err = sess.Insert(&app)
 			return err
 			return err
 		} else {
 		} else {
+			for key, data := range cmd.SecureJsonData {
+				app.SecureJsonData[key] = util.Encrypt([]byte(data), setting.SecretKey)
+			}
 			app.Updated = time.Now()
 			app.Updated = time.Now()
 			app.Enabled = cmd.Enabled
 			app.Enabled = cmd.Enabled
 			app.JsonData = cmd.JsonData
 			app.JsonData = cmd.JsonData

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

@@ -14,6 +14,7 @@ import (
 func init() {
 func init() {
 	bus.AddHandler("sql", SaveDashboard)
 	bus.AddHandler("sql", SaveDashboard)
 	bus.AddHandler("sql", GetDashboard)
 	bus.AddHandler("sql", GetDashboard)
+	bus.AddHandler("sql", GetDashboards)
 	bus.AddHandler("sql", DeleteDashboard)
 	bus.AddHandler("sql", DeleteDashboard)
 	bus.AddHandler("sql", SearchDashboards)
 	bus.AddHandler("sql", SearchDashboards)
 	bus.AddHandler("sql", GetDashboardTags)
 	bus.AddHandler("sql", GetDashboardTags)
@@ -223,3 +224,20 @@ func DeleteDashboard(cmd *m.DeleteDashboardCommand) error {
 		return nil
 		return nil
 	})
 	})
 }
 }
+
+func GetDashboards(query *m.GetDashboardsQuery) error {
+	if len(query.DashboardIds) == 0 {
+		return m.ErrCommandValidationFailed
+	}
+
+	var dashboards = make([]m.Dashboard, 0)
+
+	err := x.In("id", query.DashboardIds).Find(&dashboards)
+	query.Result = &dashboards
+
+	if err != nil {
+		return err
+	}
+
+	return nil
+}

+ 6 - 3
pkg/services/sqlstore/migrations/app_settings.go

@@ -4,7 +4,7 @@ import . "github.com/grafana/grafana/pkg/services/sqlstore/migrator"
 
 
 func addAppSettingsMigration(mg *Migrator) {
 func addAppSettingsMigration(mg *Migrator) {
 
 
-	appSettingsV1 := Table{
+	appSettingsV2 := Table{
 		Name: "app_settings",
 		Name: "app_settings",
 		Columns: []*Column{
 		Columns: []*Column{
 			{Name: "id", Type: DB_BigInt, IsPrimaryKey: true, IsAutoIncrement: true},
 			{Name: "id", Type: DB_BigInt, IsPrimaryKey: true, IsAutoIncrement: true},
@@ -13,6 +13,7 @@ func addAppSettingsMigration(mg *Migrator) {
 			{Name: "enabled", Type: DB_Bool, Nullable: false},
 			{Name: "enabled", Type: DB_Bool, Nullable: false},
 			{Name: "pinned", Type: DB_Bool, Nullable: false},
 			{Name: "pinned", Type: DB_Bool, Nullable: false},
 			{Name: "json_data", Type: DB_Text, Nullable: true},
 			{Name: "json_data", Type: DB_Text, Nullable: true},
+			{Name: "secure_json_data", Type: DB_Text, Nullable: true},
 			{Name: "created", Type: DB_DateTime, Nullable: false},
 			{Name: "created", Type: DB_DateTime, Nullable: false},
 			{Name: "updated", Type: DB_DateTime, Nullable: false},
 			{Name: "updated", Type: DB_DateTime, Nullable: false},
 		},
 		},
@@ -21,8 +22,10 @@ func addAppSettingsMigration(mg *Migrator) {
 		},
 		},
 	}
 	}
 
 
-	mg.AddMigration("create app_settings table v1", NewAddTableMigration(appSettingsV1))
+	mg.AddMigration("Drop old table app_settings v1", NewDropTableMigration("app_settings"))
+
+	mg.AddMigration("create app_settings table v2", NewAddTableMigration(appSettingsV2))
 
 
 	//-------  indexes ------------------
 	//-------  indexes ------------------
-	addTableIndicesMigrations(mg, "v3", appSettingsV1)
+	addTableIndicesMigrations(mg, "v3", appSettingsV2)
 }
 }

+ 0 - 18
pkg/services/sqlstore/playlist.go

@@ -15,7 +15,6 @@ func init() {
 	bus.AddHandler("sql", DeletePlaylist)
 	bus.AddHandler("sql", DeletePlaylist)
 	bus.AddHandler("sql", SearchPlaylists)
 	bus.AddHandler("sql", SearchPlaylists)
 	bus.AddHandler("sql", GetPlaylist)
 	bus.AddHandler("sql", GetPlaylist)
-	bus.AddHandler("sql", GetPlaylistDashboards)
 	bus.AddHandler("sql", GetPlaylistItem)
 	bus.AddHandler("sql", GetPlaylistItem)
 }
 }
 
 
@@ -162,20 +161,3 @@ func GetPlaylistItem(query *m.GetPlaylistItemsByIdQuery) error {
 
 
 	return err
 	return err
 }
 }
-
-func GetPlaylistDashboards(query *m.GetPlaylistDashboardsQuery) error {
-	if len(query.DashboardIds) == 0 {
-		return m.ErrCommandValidationFailed
-	}
-
-	var dashboards = make(m.PlaylistDashboards, 0)
-
-	err := x.In("id", query.DashboardIds).Find(&dashboards)
-	query.Result = &dashboards
-
-	if err != nil {
-		return err
-	}
-
-	return nil
-}

+ 44 - 0
pkg/services/sqlstore/playlist_test.go

@@ -0,0 +1,44 @@
+package sqlstore
+
+import (
+	"testing"
+
+	. "github.com/smartystreets/goconvey/convey"
+
+	m "github.com/grafana/grafana/pkg/models"
+)
+
+func TestPlaylistDataAccess(t *testing.T) {
+
+	Convey("Testing Playlist data access", t, func() {
+		InitTestDB(t)
+
+		Convey("Can create playlist", func() {
+			items := []m.PlaylistItemDTO{
+				{Title: "graphite", Value: "graphite", Type: "dashboard_by_tag"},
+				{Title: "Backend response times", Value: "3", Type: "dashboard_by_id"},
+			}
+			cmd := m.CreatePlaylistCommand{Name: "NYC office", Interval: "10m", OrgId: 1, Items: items}
+			err := CreatePlaylist(&cmd)
+			So(err, ShouldBeNil)
+
+			Convey("can update playlist", func() {
+				items := []m.PlaylistItemDTO{
+					{Title: "influxdb", Value: "influxdb", Type: "dashboard_by_tag"},
+					{Title: "Backend response times", Value: "2", Type: "dashboard_by_id"},
+				}
+				query := m.UpdatePlaylistCommand{Name: "NYC office ", OrgId: 1, Id: 1, Interval: "10s", Items: items}
+				err = UpdatePlaylist(&query)
+
+				So(err, ShouldBeNil)
+
+				Convey("can remove playlist", func() {
+					query := m.DeletePlaylistCommand{Id: 1}
+					err = DeletePlaylist(&query)
+
+					So(err, ShouldBeNil)
+				})
+			})
+		})
+	})
+}

+ 2 - 2
pkg/services/sqlstore/stats.go

@@ -71,7 +71,7 @@ func GetAdminStats(query *m.GetAdminStatsQuery) error {
         FROM ` + dialect.Quote("dashboard_snapshot") + `
         FROM ` + dialect.Quote("dashboard_snapshot") + `
       ) AS db_snapshot_count,
       ) AS db_snapshot_count,
       (
       (
-        SELECT COUNT(*)
+        SELECT COUNT( DISTINCT ( ` + dialect.Quote("term") + ` ))
         FROM ` + dialect.Quote("dashboard_tag") + `
         FROM ` + dialect.Quote("dashboard_tag") + `
       ) AS db_tag_count,
       ) AS db_tag_count,
       (
       (
@@ -83,7 +83,7 @@ func GetAdminStats(query *m.GetAdminStatsQuery) error {
         FROM ` + dialect.Quote("playlist") + `
         FROM ` + dialect.Quote("playlist") + `
       ) AS playlist_count,
       ) AS playlist_count,
       (
       (
-        SELECT COUNT (DISTINCT ` + dialect.Quote("dashboard_id") + ` )
+        SELECT COUNT(DISTINCT ` + dialect.Quote("dashboard_id") + ` )
         FROM ` + dialect.Quote("star") + `
         FROM ` + dialect.Quote("star") + `
       ) AS starred_db_count,
       ) AS starred_db_count,
       (
       (

+ 66 - 0
pkg/util/encryption.go

@@ -0,0 +1,66 @@
+package util
+
+import (
+	"crypto/aes"
+	"crypto/cipher"
+	"crypto/rand"
+	"crypto/sha256"
+	"io"
+
+	"github.com/grafana/grafana/pkg/log"
+)
+
+const saltLength = 8
+
+func Decrypt(payload []byte, secret string) []byte {
+	salt := payload[:saltLength]
+	key := encryptionKeyToBytes(secret, string(salt))
+
+	block, err := aes.NewCipher(key)
+	if err != nil {
+		log.Fatal(4, err.Error())
+	}
+
+	// The IV needs to be unique, but not secure. Therefore it's common to
+	// include it at the beginning of the ciphertext.
+	if len(payload) < aes.BlockSize {
+		log.Fatal(4, "payload too short")
+	}
+	iv := payload[saltLength : saltLength+aes.BlockSize]
+	payload = payload[saltLength+aes.BlockSize:]
+
+	stream := cipher.NewCFBDecrypter(block, iv)
+
+	// XORKeyStream can work in-place if the two arguments are the same.
+	stream.XORKeyStream(payload, payload)
+	return payload
+}
+
+func Encrypt(payload []byte, secret string) []byte {
+	salt := GetRandomString(saltLength)
+
+	key := encryptionKeyToBytes(secret, salt)
+	block, err := aes.NewCipher(key)
+	if err != nil {
+		log.Fatal(4, err.Error())
+	}
+
+	// The IV needs to be unique, but not secure. Therefore it's common to
+	// include it at the beginning of the ciphertext.
+	ciphertext := make([]byte, saltLength+aes.BlockSize+len(payload))
+	copy(ciphertext[:saltLength], []byte(salt))
+	iv := ciphertext[saltLength : saltLength+aes.BlockSize]
+	if _, err := io.ReadFull(rand.Reader, iv); err != nil {
+		log.Fatal(4, err.Error())
+	}
+
+	stream := cipher.NewCFBEncrypter(block, iv)
+	stream.XORKeyStream(ciphertext[saltLength+aes.BlockSize:], payload)
+
+	return ciphertext
+}
+
+// Key needs to be 32bytes
+func encryptionKeyToBytes(secret, salt string) []byte {
+	return PBKDF2([]byte(secret), []byte(salt), 10000, 32, sha256.New)
+}

+ 27 - 0
pkg/util/encryption_test.go

@@ -0,0 +1,27 @@
+package util
+
+import (
+	"testing"
+
+	. "github.com/smartystreets/goconvey/convey"
+)
+
+func TestEncryption(t *testing.T) {
+
+	Convey("When getting encryption key", t, func() {
+
+		key := encryptionKeyToBytes("secret", "salt")
+		So(len(key), ShouldEqual, 32)
+
+		key = encryptionKeyToBytes("a very long secret key that is larger then 32bytes", "salt")
+		So(len(key), ShouldEqual, 32)
+	})
+
+	Convey("When decrypting basic payload", t, func() {
+		encrypted := Encrypt([]byte("grafana"), "1234")
+		decrypted := Decrypt(encrypted, "1234")
+
+		So(string(decrypted), ShouldEqual, "grafana")
+	})
+
+}

+ 5 - 0
pkg/util/url.go

@@ -27,6 +27,11 @@ func (r *UrlQueryReader) Get(name string, def string) string {
 func JoinUrlFragments(a, b string) string {
 func JoinUrlFragments(a, b string) string {
 	aslash := strings.HasSuffix(a, "/")
 	aslash := strings.HasSuffix(a, "/")
 	bslash := strings.HasPrefix(b, "/")
 	bslash := strings.HasPrefix(b, "/")
+
+	if len(b) == 0 {
+		return a
+	}
+
 	switch {
 	switch {
 	case aslash && bslash:
 	case aslash && bslash:
 		return a + b[1:]
 		return a + b[1:]

+ 46 - 0
pkg/util/url_test.go

@@ -0,0 +1,46 @@
+package util
+
+import (
+	"testing"
+
+	. "github.com/smartystreets/goconvey/convey"
+)
+
+func TestUrl(t *testing.T) {
+
+	Convey("When joining two urls where right hand side is empty", t, func() {
+		result := JoinUrlFragments("http://localhost:8080", "")
+
+		So(result, ShouldEqual, "http://localhost:8080")
+	})
+
+	Convey("When joining two urls where right hand side is empty and lefthand side has a trailing slash", t, func() {
+		result := JoinUrlFragments("http://localhost:8080/", "")
+
+		So(result, ShouldEqual, "http://localhost:8080/")
+	})
+
+	Convey("When joining two urls where neither has a trailing slash", t, func() {
+		result := JoinUrlFragments("http://localhost:8080", "api")
+
+		So(result, ShouldEqual, "http://localhost:8080/api")
+	})
+
+	Convey("When joining two urls where lefthand side has a trailing slash", t, func() {
+		result := JoinUrlFragments("http://localhost:8080/", "api")
+
+		So(result, ShouldEqual, "http://localhost:8080/api")
+	})
+
+	Convey("When joining two urls where righthand side has preceding slash", t, func() {
+		result := JoinUrlFragments("http://localhost:8080", "/api")
+
+		So(result, ShouldEqual, "http://localhost:8080/api")
+	})
+
+	Convey("When joining two urls where righthand side has trailing slash", t, func() {
+		result := JoinUrlFragments("http://localhost:8080", "api/")
+
+		So(result, ShouldEqual, "http://localhost:8080/api/")
+	})
+}

+ 11 - 2
public/app/core/components/grafana_app.ts

@@ -5,7 +5,7 @@ import store from 'app/core/store';
 import _ from 'lodash';
 import _ from 'lodash';
 import angular from 'angular';
 import angular from 'angular';
 import $ from 'jquery';
 import $ from 'jquery';
-import coreModule from '../core_module';
+import coreModule from 'app/core/core_module';
 
 
 export class GrafanaCtrl {
 export class GrafanaCtrl {
 
 
@@ -150,6 +150,9 @@ export function grafanaAppDirective(playlistSrv) {
       scope.$watch('contextSrv.sidemenu', newVal => {
       scope.$watch('contextSrv.sidemenu', newVal => {
         if (newVal !== undefined) {
         if (newVal !== undefined) {
           elem.toggleClass('sidemenu-open', scope.contextSrv.sidemenu);
           elem.toggleClass('sidemenu-open', scope.contextSrv.sidemenu);
+          if (!newVal) {
+            scope.contextSrv.setPinnedState(false);
+          }
         }
         }
         if (scope.contextSrv.sidemenu) {
         if (scope.contextSrv.sidemenu) {
           ignoreSideMenuHide = true;
           ignoreSideMenuHide = true;
@@ -159,6 +162,12 @@ export function grafanaAppDirective(playlistSrv) {
         }
         }
       });
       });
 
 
+      scope.$watch('contextSrv.pinned', newVal => {
+        if (newVal !== undefined) {
+          elem.toggleClass('sidemenu-pinned', newVal);
+        }
+      });
+
       // tooltip removal fix
       // tooltip removal fix
       scope.$on("$routeChangeSuccess", function() {
       scope.$on("$routeChangeSuccess", function() {
         $("#tooltip, .tooltip").remove();
         $("#tooltip, .tooltip").remove();
@@ -182,7 +191,7 @@ export function grafanaAppDirective(playlistSrv) {
           }
           }
         }
         }
         // hide sidemenu
         // hide sidemenu
-        if (!ignoreSideMenuHide &&  elem.find('.sidemenu').length > 0) {
+        if (!ignoreSideMenuHide && !scope.contextSrv.pinned && elem.find('.sidemenu').length > 0) {
           if (target.parents('.sidemenu').length === 0) {
           if (target.parents('.sidemenu').length === 0) {
             scope.$apply(() => scope.contextSrv.toggleSideMenu());
             scope.$apply(() => scope.contextSrv.toggleSideMenu());
           }
           }

+ 3 - 2
public/app/core/components/navbar/navbar.html

@@ -1,10 +1,11 @@
-<div class="navbar navbar-static-top">
+<div class="navbar">
 	<div class="navbar-inner"><div class="container-fluid">
 	<div class="navbar-inner"><div class="container-fluid">
 			<div class="top-nav-btn top-nav-menu-btn">
 			<div class="top-nav-btn top-nav-menu-btn">
 				<a class="pointer" ng-click="ctrl.contextSrv.toggleSideMenu()">
 				<a class="pointer" ng-click="ctrl.contextSrv.toggleSideMenu()">
 					<span class="top-nav-logo-background">
 					<span class="top-nav-logo-background">
-						<img class="logo-icon" src="img/fav32.png"></img>
+						<img class="logo-icon" src="img/grafana_icon.svg"></img>
 					</span>
 					</span>
+					<i class="icon-gf icon-gf-grafana_wordmark"></i>
 					<i class="fa fa-caret-down"></i>
 					<i class="fa fa-caret-down"></i>
 				</a>
 				</a>
 			</div>
 			</div>

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

@@ -14,7 +14,7 @@ export class NavbarCtrl {
 export function navbarDirective() {
 export function navbarDirective() {
   return {
   return {
     restrict: 'E',
     restrict: 'E',
-    templateUrl: 'app/core/components/navbar/navbar.html',
+    templateUrl: 'public/app/core/components/navbar/navbar.html',
     controller: NavbarCtrl,
     controller: NavbarCtrl,
     bindToController: true,
     bindToController: true,
     controllerAs: 'ctrl',
     controllerAs: 'ctrl',

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

@@ -137,7 +137,7 @@ export class SearchCtrl {
 export function searchDirective() {
 export function searchDirective() {
   return {
   return {
     restrict: 'E',
     restrict: 'E',
-    templateUrl: 'app/core/components/search/search.html',
+    templateUrl: 'public/app/core/components/search/search.html',
     controller: SearchCtrl,
     controller: SearchCtrl,
     bindToController: true,
     bindToController: true,
     controllerAs: 'ctrl',
     controllerAs: 'ctrl',

+ 6 - 0
public/app/core/components/sidemenu/sidemenu.html

@@ -62,5 +62,11 @@
 		</a>
 		</a>
 	</li>
 	</li>
 
 
+	<li>
+		<a class="sidemenu-item" target="_self" ng-hide="ctrl.contextSrv.pinned" ng-click="ctrl.contextSrv.setPinnedState(true)">
+			<span class="icon-circle sidemenu-icon"><i class="fa fa-fw fa-thumb-tack"></i></span>
+			<span class="sidemenu-item-text">Pin</span>
+		</a>
+	</li>
 </ul>
 </ul>
 
 

+ 13 - 9
public/app/core/components/sidemenu/sidemenu.ts

@@ -22,8 +22,12 @@ export class SideMenuCtrl {
     this.appSubUrl = config.appSubUrl;
     this.appSubUrl = config.appSubUrl;
     this.showSignout = this.contextSrv.isSignedIn && !config['authProxyEnabled'];
     this.showSignout = this.contextSrv.isSignedIn && !config['authProxyEnabled'];
     this.updateMenu();
     this.updateMenu();
+
     this.$scope.$on('$routeChangeSuccess', () => {
     this.$scope.$on('$routeChangeSuccess', () => {
-      this.contextSrv.sidemenu = false;
+      this.updateMenu();
+      if (!this.contextSrv.pinned) {
+        this.contextSrv.sidemenu = false;
+      }
     });
     });
   }
   }
 
 
@@ -83,11 +87,11 @@ export class SideMenuCtrl {
            this.switchOrg(org.orgId);
            this.switchOrg(org.orgId);
          }
          }
        });
        });
-
-       if (config.allowOrgCreate) {
-         this.orgMenu.push({text: "New organization", icon: "fa fa-fw fa-plus", url: this.getUrl('/org/new')});
-       }
      });
      });
+
+     if (config.allowOrgCreate) {
+       this.orgMenu.push({text: "New organization", icon: "fa fa-fw fa-plus", url: this.getUrl('/org/new')});
+     }
    });
    });
  }
  }
 
 
@@ -108,19 +112,19 @@ export class SideMenuCtrl {
    });
    });
 
 
    this.mainLinks.push({
    this.mainLinks.push({
-     text: "Grafana stats",
+     text: "Stats",
      icon: "fa fa-fw fa-bar-chart",
      icon: "fa fa-fw fa-bar-chart",
      url: this.getUrl("/admin/stats"),
      url: this.getUrl("/admin/stats"),
    });
    });
 
 
    this.mainLinks.push({
    this.mainLinks.push({
-     text: "Global Users",
+     text: "Users",
      icon: "fa fa-fw fa-user",
      icon: "fa fa-fw fa-user",
      url: this.getUrl("/admin/users"),
      url: this.getUrl("/admin/users"),
    });
    });
 
 
    this.mainLinks.push({
    this.mainLinks.push({
-     text: "Global Orgs",
+     text: "Organizations",
      icon: "fa fa-fw fa-users",
      icon: "fa fa-fw fa-users",
      url: this.getUrl("/admin/orgs"),
      url: this.getUrl("/admin/orgs"),
    });
    });
@@ -144,7 +148,7 @@ export class SideMenuCtrl {
 export function sideMenuDirective() {
 export function sideMenuDirective() {
   return {
   return {
     restrict: 'E',
     restrict: 'E',
-    templateUrl: 'app/core/components/sidemenu/sidemenu.html',
+    templateUrl: 'public/app/core/components/sidemenu/sidemenu.html',
     controller: SideMenuCtrl,
     controller: SideMenuCtrl,
     bindToController: true,
     bindToController: true,
     controllerAs: 'ctrl',
     controllerAs: 'ctrl',

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

@@ -15,7 +15,6 @@ import "./directives/ng_model_on_blur";
 import "./directives/password_strenght";
 import "./directives/password_strenght";
 import "./directives/spectrum_picker";
 import "./directives/spectrum_picker";
 import "./directives/tags";
 import "./directives/tags";
-import "./directives/topnav";
 import "./directives/value_select_dropdown";
 import "./directives/value_select_dropdown";
 import "./directives/give_focus";
 import "./directives/give_focus";
 import './jquery_extended';
 import './jquery_extended';

+ 3 - 3
public/app/core/directives/dash_edit_link.js

@@ -6,9 +6,9 @@ function ($, coreModule) {
   'use strict';
   'use strict';
 
 
   var editViewMap = {
   var editViewMap = {
-    'settings':    { src: 'app/features/dashboard/partials/settings.html', title: "Settings" },
-    'annotations': { src: 'app/features/annotations/partials/editor.html', title: "Annotations" },
-    'templating':  { src: 'app/features/templating/partials/editor.html', title: "Templating" }
+    'settings':    { src: 'public/app/features/dashboard/partials/settings.html', title: "Settings" },
+    'annotations': { src: 'public/app/features/annotations/partials/editor.html', title: "Annotations" },
+    'templating':  { src: 'public/app/features/templating/partials/editor.html', title: "Templating" }
   };
   };
 
 
   coreModule.default.directive('dashEditorLink', function($timeout) {
   coreModule.default.directive('dashEditorLink', function($timeout) {

+ 0 - 50
public/app/core/directives/topnav.js

@@ -1,50 +0,0 @@
-define([
-  '../core_module',
-],
-function (coreModule) {
-  'use strict';
-
-  coreModule.default.directive('topnav', function($rootScope, contextSrv) {
-    return {
-      restrict: 'E',
-      transclude: true,
-      scope: {
-        title: "@",
-        section: "@",
-        titleUrl: "@",
-        subnav: "=",
-      },
-      template:
-        '<div class="navbar navbar-static-top"><div class="navbar-inner"><div class="container-fluid">' +
-        '<div class="top-nav">' +
-				'<div class="top-nav-btn top-nav-menu-btn">' +
-					'<a class="pointer" ng-click="contextSrv.toggleSideMenu()">' +
-						'<span class="top-nav-logo-background">' +
-							'<img class="logo-icon" src="img/fav32.png"></img>' +
-						'</span>' +
-						'<i class="fa fa-caret-down"></i>' +
-					'</a>' +
-				'</div>' +
-
-        '<span class="icon-circle top-nav-icon">' +
-        '<i ng-class="icon"></i>' +
-        '</span>' +
-
-        '<span ng-show="section">' +
-        '<span class="top-nav-title">{{section}}</span>' +
-        '<i class="top-nav-breadcrumb-icon fa fa-angle-right"></i>' +
-        '</span>' +
-
-        '<a ng-href="{{titleUrl}}" class="top-nav-title">' +
-        '{{title}}' +
-        '</a>' +
-        '<i ng-show="subnav" class="top-nav-breadcrumb-icon fa fa-angle-right"></i>' +
-        '</div><div ng-transclude></div></div></div></div>',
-      link: function(scope, elem, attrs) {
-        scope.icon = attrs.icon;
-        scope.contextSrv = contextSrv;
-      }
-    };
-  });
-
-});

+ 1 - 1
public/app/core/directives/value_select_dropdown.js

@@ -227,7 +227,7 @@ function (angular, _, coreModule) {
   coreModule.default.directive('valueSelectDropdown', function($compile, $window, $timeout, $rootScope) {
   coreModule.default.directive('valueSelectDropdown', function($compile, $window, $timeout, $rootScope) {
     return {
     return {
       scope: { variable: "=", onUpdated: "&", getValuesForTag: "&" },
       scope: { variable: "=", onUpdated: "&", getValuesForTag: "&" },
-      templateUrl: 'app/partials/valueSelectDropdown.html',
+      templateUrl: 'public/app/partials/valueSelectDropdown.html',
       controller: 'ValueSelectDropdownCtrl',
       controller: 'ValueSelectDropdownCtrl',
       controllerAs: 'vm',
       controllerAs: 'vm',
       bindToController: true,
       bindToController: true,

+ 6 - 3
public/app/core/filters/filters.ts

@@ -59,11 +59,14 @@ coreModule.filter('noXml', function() {
 
 
 coreModule.filter('interpolateTemplateVars', function (templateSrv) {
 coreModule.filter('interpolateTemplateVars', function (templateSrv) {
   var filterFunc: any = function(text, scope) {
   var filterFunc: any = function(text, scope) {
-    if (scope.panel) {
-      return templateSrv.replaceWithText(text, scope.panel.scopedVars);
+    var scopedVars;
+    if (scope.ctrl && scope.ctrl.panel) {
+      scopedVars = scope.ctrl.panel.scopedVars;
     } else {
     } else {
-      return templateSrv.replaceWithText(text, scope.row.scopedVars);
+      scopedVars = scope.row.scopedVars;
     }
     }
+
+    return templateSrv.replaceWithText(text, scopedVars);
   };
   };
 
 
   filterFunc.$stateful = true;
   filterFunc.$stateful = true;

+ 32 - 32
public/app/core/routes/all.js

@@ -14,146 +14,146 @@ define([
 
 
     $routeProvider
     $routeProvider
       .when('/', {
       .when('/', {
-        templateUrl: 'app/partials/dashboard.html',
+        templateUrl: 'public/app/partials/dashboard.html',
         controller : 'LoadDashboardCtrl',
         controller : 'LoadDashboardCtrl',
         reloadOnSearch: false,
         reloadOnSearch: false,
       })
       })
       .when('/dashboard/:type/:slug', {
       .when('/dashboard/:type/:slug', {
-        templateUrl: 'app/partials/dashboard.html',
+        templateUrl: 'public/app/partials/dashboard.html',
         controller : 'LoadDashboardCtrl',
         controller : 'LoadDashboardCtrl',
         reloadOnSearch: false,
         reloadOnSearch: false,
       })
       })
       .when('/dashboard-solo/:type/:slug', {
       .when('/dashboard-solo/:type/:slug', {
-        templateUrl: 'app/features/panel/partials/soloPanel.html',
+        templateUrl: 'public/app/features/panel/partials/soloPanel.html',
         controller : 'SoloPanelCtrl',
         controller : 'SoloPanelCtrl',
       })
       })
       .when('/dashboard-import/:file', {
       .when('/dashboard-import/:file', {
-        templateUrl: 'app/partials/dashboard.html',
+        templateUrl: 'public/app/partials/dashboard.html',
         controller : 'DashFromImportCtrl',
         controller : 'DashFromImportCtrl',
         reloadOnSearch: false,
         reloadOnSearch: false,
       })
       })
       .when('/dashboard/new', {
       .when('/dashboard/new', {
-        templateUrl: 'app/partials/dashboard.html',
+        templateUrl: 'public/app/partials/dashboard.html',
         controller : 'NewDashboardCtrl',
         controller : 'NewDashboardCtrl',
         reloadOnSearch: false,
         reloadOnSearch: false,
       })
       })
       .when('/import/dashboard', {
       .when('/import/dashboard', {
-        templateUrl: 'app/features/dashboard/partials/import.html',
+        templateUrl: 'public/app/features/dashboard/partials/import.html',
         controller : 'DashboardImportCtrl',
         controller : 'DashboardImportCtrl',
       })
       })
       .when('/datasources', {
       .when('/datasources', {
-        templateUrl: 'app/features/datasources/partials/list.html',
+        templateUrl: 'public/app/features/datasources/partials/list.html',
         controller : 'DataSourcesCtrl',
         controller : 'DataSourcesCtrl',
         resolve: loadOrgBundle,
         resolve: loadOrgBundle,
       })
       })
       .when('/datasources/edit/:id', {
       .when('/datasources/edit/:id', {
-        templateUrl: 'app/features/datasources/partials/edit.html',
+        templateUrl: 'public/app/features/datasources/partials/edit.html',
         controller : 'DataSourceEditCtrl',
         controller : 'DataSourceEditCtrl',
         resolve: loadOrgBundle,
         resolve: loadOrgBundle,
       })
       })
       .when('/datasources/new', {
       .when('/datasources/new', {
-        templateUrl: 'app/features/datasources/partials/edit.html',
+        templateUrl: 'public/app/features/datasources/partials/edit.html',
         controller : 'DataSourceEditCtrl',
         controller : 'DataSourceEditCtrl',
         resolve: loadOrgBundle,
         resolve: loadOrgBundle,
       })
       })
       .when('/org', {
       .when('/org', {
-        templateUrl: 'app/features/org/partials/orgDetails.html',
+        templateUrl: 'public/app/features/org/partials/orgDetails.html',
         controller : 'OrgDetailsCtrl',
         controller : 'OrgDetailsCtrl',
         resolve: loadOrgBundle,
         resolve: loadOrgBundle,
       })
       })
       .when('/org/new', {
       .when('/org/new', {
-        templateUrl: 'app/features/org/partials/newOrg.html',
+        templateUrl: 'public/app/features/org/partials/newOrg.html',
         controller : 'NewOrgCtrl',
         controller : 'NewOrgCtrl',
         resolve: loadOrgBundle,
         resolve: loadOrgBundle,
       })
       })
       .when('/org/users', {
       .when('/org/users', {
-        templateUrl: 'app/features/org/partials/orgUsers.html',
+        templateUrl: 'public/app/features/org/partials/orgUsers.html',
         controller : 'OrgUsersCtrl',
         controller : 'OrgUsersCtrl',
         resolve: loadOrgBundle,
         resolve: loadOrgBundle,
       })
       })
       .when('/org/apikeys', {
       .when('/org/apikeys', {
-        templateUrl: 'app/features/org/partials/orgApiKeys.html',
+        templateUrl: 'public/app/features/org/partials/orgApiKeys.html',
         controller : 'OrgApiKeysCtrl',
         controller : 'OrgApiKeysCtrl',
         resolve: loadOrgBundle,
         resolve: loadOrgBundle,
       })
       })
       .when('/profile', {
       .when('/profile', {
-        templateUrl: 'app/features/profile/partials/profile.html',
+        templateUrl: 'public/app/features/profile/partials/profile.html',
         controller : 'ProfileCtrl',
         controller : 'ProfileCtrl',
       })
       })
       .when('/profile/password', {
       .when('/profile/password', {
-        templateUrl: 'app/features/profile/partials/password.html',
+        templateUrl: 'public/app/features/profile/partials/password.html',
         controller : 'ChangePasswordCtrl',
         controller : 'ChangePasswordCtrl',
       })
       })
       .when('/profile/select-org', {
       .when('/profile/select-org', {
-        templateUrl: 'app/features/profile/partials/select_org.html',
+        templateUrl: 'public/app/features/profile/partials/select_org.html',
         controller : 'SelectOrgCtrl',
         controller : 'SelectOrgCtrl',
       })
       })
       .when('/admin/settings', {
       .when('/admin/settings', {
-        templateUrl: 'app/features/admin/partials/settings.html',
+        templateUrl: 'public/app/features/admin/partials/settings.html',
         controller : 'AdminSettingsCtrl',
         controller : 'AdminSettingsCtrl',
       })
       })
       .when('/admin/users', {
       .when('/admin/users', {
-        templateUrl: 'app/features/admin/partials/users.html',
+        templateUrl: 'public/app/features/admin/partials/users.html',
         controller : 'AdminListUsersCtrl',
         controller : 'AdminListUsersCtrl',
       })
       })
       .when('/admin/users/create', {
       .when('/admin/users/create', {
-        templateUrl: 'app/features/admin/partials/new_user.html',
+        templateUrl: 'public/app/features/admin/partials/new_user.html',
         controller : 'AdminEditUserCtrl',
         controller : 'AdminEditUserCtrl',
       })
       })
       .when('/admin/users/edit/:id', {
       .when('/admin/users/edit/:id', {
-        templateUrl: 'app/features/admin/partials/edit_user.html',
+        templateUrl: 'public/app/features/admin/partials/edit_user.html',
         controller : 'AdminEditUserCtrl',
         controller : 'AdminEditUserCtrl',
       })
       })
       .when('/admin/orgs', {
       .when('/admin/orgs', {
-        templateUrl: 'app/features/admin/partials/orgs.html',
+        templateUrl: 'public/app/features/admin/partials/orgs.html',
         controller : 'AdminListOrgsCtrl',
         controller : 'AdminListOrgsCtrl',
       })
       })
       .when('/admin/orgs/edit/:id', {
       .when('/admin/orgs/edit/:id', {
-        templateUrl: 'app/features/admin/partials/edit_org.html',
+        templateUrl: 'public/app/features/admin/partials/edit_org.html',
         controller : 'AdminEditOrgCtrl',
         controller : 'AdminEditOrgCtrl',
       })
       })
       .when('/admin/stats', {
       .when('/admin/stats', {
-        templateUrl: 'app/features/admin/partials/stats.html',
+        templateUrl: 'public/app/features/admin/partials/stats.html',
         controller : 'AdminStatsCtrl',
         controller : 'AdminStatsCtrl',
         controllerAs: 'ctrl',
         controllerAs: 'ctrl',
       })
       })
       .when('/login', {
       .when('/login', {
-        templateUrl: 'app/partials/login.html',
+        templateUrl: 'public/app/partials/login.html',
         controller : 'LoginCtrl',
         controller : 'LoginCtrl',
       })
       })
       .when('/invite/:code', {
       .when('/invite/:code', {
-        templateUrl: 'app/partials/signup_invited.html',
+        templateUrl: 'public/app/partials/signup_invited.html',
         controller : 'InvitedCtrl',
         controller : 'InvitedCtrl',
       })
       })
       .when('/signup', {
       .when('/signup', {
-        templateUrl: 'app/partials/signup_step2.html',
+        templateUrl: 'public/app/partials/signup_step2.html',
         controller : 'SignUpCtrl',
         controller : 'SignUpCtrl',
       })
       })
       .when('/user/password/send-reset-email', {
       .when('/user/password/send-reset-email', {
-        templateUrl: 'app/partials/reset_password.html',
+        templateUrl: 'public/app/partials/reset_password.html',
         controller : 'ResetPasswordCtrl',
         controller : 'ResetPasswordCtrl',
       })
       })
       .when('/user/password/reset', {
       .when('/user/password/reset', {
-        templateUrl: 'app/partials/reset_password.html',
+        templateUrl: 'public/app/partials/reset_password.html',
         controller : 'ResetPasswordCtrl',
         controller : 'ResetPasswordCtrl',
       })
       })
       .when('/apps', {
       .when('/apps', {
-        templateUrl: 'app/features/apps/partials/list.html',
+        templateUrl: 'public/app/features/apps/partials/list.html',
         controller: 'AppListCtrl',
         controller: 'AppListCtrl',
         controllerAs: 'ctrl',
         controllerAs: 'ctrl',
         resolve: loadAppsBundle,
         resolve: loadAppsBundle,
       })
       })
       .when('/apps/edit/:appId', {
       .when('/apps/edit/:appId', {
-        templateUrl: 'app/features/apps/partials/edit.html',
+        templateUrl: 'public/app/features/apps/partials/edit.html',
         controller: 'AppEditCtrl',
         controller: 'AppEditCtrl',
         controllerAs: 'ctrl',
         controllerAs: 'ctrl',
         resolve: loadAppsBundle,
         resolve: loadAppsBundle,
       })
       })
       .when('/global-alerts', {
       .when('/global-alerts', {
-        templateUrl: 'app/features/dashboard/partials/globalAlerts.html',
+        templateUrl: 'public/app/features/dashboard/partials/globalAlerts.html',
       })
       })
       .otherwise({
       .otherwise({
-        templateUrl: 'app/partials/error.html',
+        templateUrl: 'public/app/partials/error.html',
         controller: 'ErrorCtrl'
         controller: 'ErrorCtrl'
       });
       });
   });
   });

+ 1 - 1
public/app/core/services/alert_srv.js

@@ -72,7 +72,7 @@ function (angular, _, coreModule) {
       scope.noText = payload.noText || "Cancel";
       scope.noText = payload.noText || "Cancel";
 
 
       var confirmModal = $modal({
       var confirmModal = $modal({
-        template: './app/partials/confirm_modal.html',
+        template: 'public/app/partials/confirm_modal.html',
         persist: false,
         persist: false,
         modalClass: 'modal-no-header confirm-modal',
         modalClass: 'modal-no-header confirm-modal',
         show: false,
         show: false,

+ 13 - 0
public/app/core/services/context_srv.js

@@ -20,10 +20,23 @@ function (angular, _, coreModule, store, config) {
       return this.user.orgRole === role;
       return this.user.orgRole === role;
     };
     };
 
 
+    this.setPinnedState = function(val) {
+      this.pinned = val;
+      store.set('grafana.sidemenu.pinned', val);
+    };
+
     this.toggleSideMenu = function() {
     this.toggleSideMenu = function() {
       this.sidemenu = !this.sidemenu;
       this.sidemenu = !this.sidemenu;
+      if (!this.sidemenu) {
+        this.setPinnedState(false);
+      }
     };
     };
 
 
+    this.pinned = store.getBool('grafana.sidemenu.pinned', false);
+    if (this.pinned) {
+      this.sidemenu = true;
+    }
+
     this.version = config.buildInfo.version;
     this.version = config.buildInfo.version;
     this.lightTheme = false;
     this.lightTheme = false;
     this.user = new User();
     this.user = new User();

+ 3 - 4
public/app/features/admin/partials/edit_org.html

@@ -1,9 +1,8 @@
-<topnav icon="fa fa-fw fa-user" title="Global Users" subnav="true">
+<navbar icon="fa fa-fw fa-user" title="Organizations" title-url="admin/orgs" subnav="true">
 	<ul class="nav">
 	<ul class="nav">
-		<li><a href="admin/orgs">List</a></li>
-		<li class="active"><a href="admin/orgs/edit/{{org.id}}">Edit Org</a></li>
+		<li class="active"><a href="admin/orgs/edit/{{org.id}}">{{org.name}}</a></li>
 	</ul>
 	</ul>
-</topnav>
+</navbar>
 
 
 <div class="page-container">
 <div class="page-container">
 	<div class="page">
 	<div class="page">

+ 2 - 4
public/app/features/admin/partials/edit_user.html

@@ -1,10 +1,8 @@
-<topnav icon="fa fa-fw fa-user" title="Global Users" subnav="true">
+<navbar icon="fa fa-fw fa-user" title="Users" title-url="admin/users" subnav="true">
 	<ul class="nav">
 	<ul class="nav">
-		<li><a href="admin/users">Users</a></li>
-		<li><a href="admin/users/create">Create user</a></li>
 		<li class="active"><a href="admin/users/edit/{{user_id}}">Edit user</a></li>
 		<li class="active"><a href="admin/users/edit/{{user_id}}">Edit user</a></li>
 	</ul>
 	</ul>
-</topnav>
+</navbar>
 
 
 <div class="page-container">
 <div class="page-container">
 	<div class="page">
 	<div class="page">

+ 4 - 5
public/app/features/admin/partials/new_user.html

@@ -1,14 +1,13 @@
-<topnav icon="fa fa-fw fa-cogs" title="Global Users" subnav="true">
+<navbar icon="fa fa-fw fa-user" title="Users" title-url="admin/users" subnav="true">
 	<ul class="nav">
 	<ul class="nav">
-		<li><a href="admin/users">Users</a></li>
-		<li class="active"><a href="admin/users/create">Create user</a></li>
+		<li class="active"><a href="admin/users/create">Add user</a></li>
 	</ul>
 	</ul>
-</topnav>
+</navbar>
 
 
 <div class="page-container">
 <div class="page-container">
 	<div class="page">
 	<div class="page">
 		<h2>
 		<h2>
-			Create a new user
+			Add new user
 		</h2>
 		</h2>
 
 
 		<form name="userForm">
 		<form name="userForm">

+ 2 - 5
public/app/features/admin/partials/orgs.html

@@ -1,8 +1,5 @@
-<topnav icon="fa fa-fw fa-users" title="Global Orgs" subnav="true">
-	<ul class="nav">
-		<li class="active"><a href="admin/orgs">List</a></li>
-	</ul>
-</topnav>
+<navbar icon="fa fa-fw fa-users" title="Organizations">
+</navbar>
 
 
 <div class="page-container">
 <div class="page-container">
 	<div class="page-wide">
 	<div class="page-wide">

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

@@ -1,5 +1,5 @@
-<topnav icon="fa fa-fw fa-info" title="System info">
-</topnav>
+<navbar icon="fa fa-fw fa-info" title="System info">
+</navbar>
 
 
 <div class="page-container">
 <div class="page-container">
 	<div class="page">
 	<div class="page">

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

@@ -1,8 +1,5 @@
-<topnav icon="fa fa-fw fa-bar-chart" title="Grafana stats"  subnav="true">
-  <ul class="nav">
-    <li class="active"><a href="admin/stats">Overview</a></li>
-  </ul>
-</topnav>
+<navbar icon="fa fa-fw fa-bar-chart" title="Stats">
+</navbar>
 
 
 <div class="page-container">
 <div class="page-container">
 	<div class="page-wide" ng-init="ctrl.init()">
 	<div class="page-wide" ng-init="ctrl.init()">

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

@@ -1,15 +1,14 @@
-<topnav icon="fa fa-fw fa-user" title="Global Users" subnav="true">
-	<ul class="nav">
-		<li class="active"><a href="admin/users">List</a></li>
-		<li><a href="admin/users/create">Create user</a></li>
-	</ul>
-</topnav>
+<navbar icon="fa fa-fw fa-user" title="Users" title-url="admin/users">
+</navbar>
 
 
 <div class="page-container">
 <div class="page-container">
 	<div class="page-wide">
 	<div class="page-wide">
-		<h1>
-			Users
-		</h1>
+		<a class="btn btn-inverse pull-right" href="admin/users/create">
+			<i class="fa fa-plus"></i>
+			Add new user
+		</a>
+
+		<h1>Users</h1>
 
 
     <table class="filter-table form-inline">
     <table class="filter-table form-inline">
 			<thead>
 			<thead>

+ 1 - 1
public/app/features/annotations/editor_ctrl.js

@@ -31,7 +31,7 @@ function (angular, _, $) {
     };
     };
 
 
     $scope.datasourceChanged = function() {
     $scope.datasourceChanged = function() {
-      datasourceSrv.get($scope.currentAnnotation.datasource).then(function(ds) {
+      return datasourceSrv.get($scope.currentAnnotation.datasource).then(function(ds) {
         $scope.currentDatasource = ds;
         $scope.currentDatasource = ds;
         $scope.currentAnnotation.datasource = ds.name;
         $scope.currentAnnotation.datasource = ds.name;
       });
       });

+ 1 - 1
public/app/features/annotations/partials/editor.html

@@ -33,9 +33,9 @@
 			<i class="fa fa-remove"></i>
 			<i class="fa fa-remove"></i>
 		</button>
 		</button>
 	</div>
 	</div>
-
 	<div class="gf-box-body">
 	<div class="gf-box-body">
 
 
+
 		<div class="editor-row row" ng-if="mode === 'list'">
 		<div class="editor-row row" ng-if="mode === 'list'">
 			<div class="span6">
 			<div class="span6">
 				<div ng-if="annotations.length === 0">
 				<div ng-if="annotations.length === 0">

+ 1 - 1
public/app/features/annotations/query_editor.ts

@@ -9,7 +9,7 @@ function annotationsQueryEditor(dynamicDirectiveSrv) {
       annotation: "=",
       annotation: "=",
       datasource: "="
       datasource: "="
     },
     },
-    watchPath: "datasource.type",
+    watchPath: "annotation.datasource",
     directive: scope => {
     directive: scope => {
       return System.import(scope.datasource.meta.module).then(function(dsModule) {
       return System.import(scope.datasource.meta.module).then(function(dsModule) {
         return {
         return {

+ 1 - 0
public/app/features/apps/edit_ctrl.ts

@@ -24,6 +24,7 @@ export class AppEditCtrl {
       enabled: this.appModel.enabled,
       enabled: this.appModel.enabled,
       pinned: this.appModel.pinned,
       pinned: this.appModel.pinned,
       jsonData: this.appModel.jsonData,
       jsonData: this.appModel.jsonData,
+      secureJsonData: this.appModel.secureJsonData,
     }, options);
     }, options);
 
 
     this.backendSrv.post(`/api/org/apps/${this.$routeParams.appId}/settings`, updateCmd).then(function() {
     this.backendSrv.post(`/api/org/apps/${this.$routeParams.appId}/settings`, updateCmd).then(function() {

+ 2 - 5
public/app/features/apps/partials/list.html

@@ -1,8 +1,5 @@
-<topnav title="Apps" icon="fa fa-fw fa-cubes" subnav="true">
-	<ul class="nav">
-		<li class="active" ><a href="org/apps">Overview</a></li>
-	</ul>
-</topnav>
+<navbar title="Apps" icon="fa fa-fw fa-cubes">
+</navbar>
 
 
 <div class="page-container">
 <div class="page-container">
   <div class="page-wide" ng-init="ctrl.init()">
   <div class="page-wide" ng-init="ctrl.init()">

+ 1 - 1
public/app/features/dashboard/dashboardCtrl.js

@@ -102,7 +102,7 @@ function (angular, $, config, moment) {
       var editScope = $rootScope.$new();
       var editScope = $rootScope.$new();
       editScope.object = options.object;
       editScope.object = options.object;
       editScope.updateHandler = options.updateHandler;
       editScope.updateHandler = options.updateHandler;
-      $scope.appEvent('show-dash-editor', { src: 'app/partials/edit_json.html', scope: editScope });
+      $scope.appEvent('show-dash-editor', { src: 'public/app/partials/edit_json.html', scope: editScope });
     };
     };
 
 
     $scope.onDrop = function(panelId, row, dropTarget) {
     $scope.onDrop = function(panelId, row, dropTarget) {

+ 15 - 3
public/app/features/dashboard/dashnav/dashnav.html

@@ -10,7 +10,7 @@
 
 
 <div class="top-nav-snapshot-title" ng-if="dashboardMeta.isSnapshot">
 <div class="top-nav-snapshot-title" ng-if="dashboardMeta.isSnapshot">
 	<a class="pointer" bs-tooltip="titleTooltip" data-placement="bottom">
 	<a class="pointer" bs-tooltip="titleTooltip" data-placement="bottom">
-		<i class="gf-icon gf-icon-snap-multi"></i>
+		<i class="icon-gf icon-gf-snapshot"></i>
 		<span class="dashboard-title">
 		<span class="dashboard-title">
 			{{dashboard.title}}
 			{{dashboard.title}}
 			<em class="small">&nbsp;&nbsp;(snapshot)</em>
 			<em class="small">&nbsp;&nbsp;(snapshot)</em>
@@ -24,8 +24,20 @@
 			<i class="fa" ng-class="{'fa-star-o': !dashboardMeta.isStarred, 'fa-star': dashboardMeta.isStarred}" style="color: orange;"></i>
 			<i class="fa" ng-class="{'fa-star-o': !dashboardMeta.isStarred, 'fa-star': dashboardMeta.isStarred}" style="color: orange;"></i>
 		</a>
 		</a>
 	</li>
 	</li>
-	<li ng-show="dashboardMeta.canShare">
-		<a class="pointer" ng-click="shareDashboard()" bs-tooltip="'Share dashboard'" data-placement="bottom"><i class="fa fa-share-square-o"></i></a>
+	<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>
+		<ul class="dropdown-menu">
+			<li ng-if="dashboardMeta.canEdit">
+				<a class="pointer" ng-click="shareDashboard(0)">
+					<i class="fa fa-link"></i> Link to Dashboard
+				</a>
+			</li>
+			<li ng-if="dashboardMeta.canEdit">
+				<a class="pointer" ng-click="shareDashboard(1)">
+					<i class="icon-gf icon-gf-snapshot"></i>Snapshot sharing
+				</a>
+			</li>
+		</ul>
 	</li>
 	</li>
 	<li ng-show="dashboardMeta.canSave">
 	<li ng-show="dashboardMeta.canSave">
 		<a ng-click="saveDashboard()" bs-tooltip="'Save dashboard'" data-placement="bottom"><i class="fa fa-save"></i></a>
 		<a ng-click="saveDashboard()" bs-tooltip="'Save dashboard'" data-placement="bottom"><i class="fa fa-save"></i></a>

+ 8 - 5
public/app/features/dashboard/dashnav/dashnav.ts

@@ -42,10 +42,13 @@ export class DashNavCtrl {
       }
       }
     };
     };
 
 
-    $scope.shareDashboard = function() {
+    $scope.shareDashboard = function(tabIndex) {
+      var modalScope = $scope.$new();
+      modalScope.tabIndex = tabIndex;
+
       $scope.appEvent('show-modal', {
       $scope.appEvent('show-modal', {
-        src: './app/features/dashboard/partials/shareModal.html',
-        scope: $scope.$new(),
+        src: 'public/app/features/dashboard/partials/shareModal.html',
+        scope: modalScope
       });
       });
     };
     };
 
 
@@ -149,7 +152,7 @@ export class DashNavCtrl {
       newScope.clone.hideControls = false;
       newScope.clone.hideControls = false;
 
 
       $scope.appEvent('show-modal', {
       $scope.appEvent('show-modal', {
-        src: './app/features/dashboard/partials/saveDashboardAs.html',
+        src: 'public/app/features/dashboard/partials/saveDashboardAs.html',
         scope: newScope,
         scope: newScope,
       });
       });
     };
     };
@@ -189,7 +192,7 @@ export class DashNavCtrl {
 export function dashNavDirective() {
 export function dashNavDirective() {
   return {
   return {
     restrict: 'E',
     restrict: 'E',
-    templateUrl: 'app/features/dashboard/dashnav/dashnav.html',
+    templateUrl: 'public/app/features/dashboard/dashnav/dashnav.html',
     controller: DashNavCtrl,
     controller: DashNavCtrl,
     transclude: true,
     transclude: true,
   };
   };

+ 1 - 1
public/app/features/dashboard/keybindings.js

@@ -21,7 +21,7 @@ function(angular, $) {
 
 
         helpModalScope = $rootScope.$new();
         helpModalScope = $rootScope.$new();
         var helpModal = $modal({
         var helpModal = $modal({
-          template: './app/partials/help_modal.html',
+          template: 'public/app/partials/help_modal.html',
           persist: false,
           persist: false,
           show: false,
           show: false,
           scope: helpModalScope,
           scope: helpModalScope,

+ 3 - 3
public/app/features/dashboard/partials/import.html

@@ -1,8 +1,8 @@
-<topnav icon="fa fa-th-large" title="Dashboards" subnav="true">
+<navbar icon="fa fa-th-large" title="Dashboards" subnav="true">
 	<ul class="nav">
 	<ul class="nav">
 		<li class="active"><a href="import/dashboard">Import</a></li>
 		<li class="active"><a href="import/dashboard">Import</a></li>
 	</ul>
 	</ul>
-</topnav>
+</navbar>
 
 
 <div class="page-container">
 <div class="page-container">
 	<div class="page">
 	<div class="page">
@@ -59,7 +59,7 @@
 			</section>
 			</section>
 		</div>
 		</div>
 
 
-		<div ng-include="'app/features/dashboard/partials/graphiteImport.html'"></div>
+		<div ng-include="'public/app/features/dashboard/partials/graphiteImport.html'"></div>
 
 
 	</div>
 	</div>
 </div>
 </div>

+ 1 - 1
public/app/features/dashboard/partials/settings.html

@@ -25,7 +25,7 @@
 							Title
 							Title
 						</li>
 						</li>
 						<li>
 						<li>
-							<input type="text" class="input-xlarge tight-form-input" ng-model='dashboard.title'></input>
+							<input type="text" class="input-large tight-form-input" ng-model='dashboard.title'></input>
 						</li>
 						</li>
 						<li class="tight-form-item">
 						<li class="tight-form-item">
 							Tags
 							Tags

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

@@ -89,7 +89,7 @@
 
 
 <script type="text/ng-template" id="shareLink.html">
 <script type="text/ng-template" id="shareLink.html">
 	<div class="share-modal-big-icon">
 	<div class="share-modal-big-icon">
-		<i class="fa fa-external-link"></i>
+		<i class="fa fa-link"></i>
 	</div>
 	</div>
 
 
 	<div ng-include src="'shareLinkOptions.html'"></div>
 	<div ng-include src="'shareLinkOptions.html'"></div>
@@ -110,7 +110,7 @@
 	<div class="ng-cloak" ng-cloak ng-controller="ShareSnapshotCtrl" ng-init="init()">
 	<div class="ng-cloak" ng-cloak ng-controller="ShareSnapshotCtrl" ng-init="init()">
 		<div class="share-modal-big-icon">
 		<div class="share-modal-big-icon">
 			<i ng-if="loading" class="fa fa-spinner fa-spin"></i>
 			<i ng-if="loading" class="fa fa-spinner fa-spin"></i>
-			<i ng-if="!loading" class="gf-icon gf-icon-snap-multi"></i>
+			<i ng-if="!loading" class="icon-gf icon-gf-snapshot"></i>
 		</div>
 		</div>
 
 
 		<div class="share-snapshot-header" ng-if="step === 1">
 		<div class="share-snapshot-header" ng-if="step === 1">

+ 0 - 29
public/app/features/dashboard/rowCtrl.js

@@ -116,36 +116,7 @@ function (angular, _, config) {
       $scope.$broadcast('render');
       $scope.$broadcast('render');
     };
     };
 
 
-    $scope.removePanel = function(panel) {
-      $scope.appEvent('confirm-modal', {
-        title: 'Are you sure you want to remove this panel?',
-        icon: 'fa-trash',
-        yesText: 'Delete',
-        onConfirm: function() {
-          $scope.row.panels = _.without($scope.row.panels, panel);
-        }
-      });
-    };
-
-    $scope.updatePanelSpan = function(panel, span) {
-      panel.span = Math.min(Math.max(Math.floor(panel.span + span), 1), 12);
-    };
-
-    $scope.replacePanel = function(newPanel, oldPanel) {
-      var row = $scope.row;
-      var index = _.indexOf(row.panels, oldPanel);
-      row.panels.splice(index, 1);
-
-      // adding it back needs to be done in next digest
-      $timeout(function() {
-        newPanel.id = oldPanel.id;
-        newPanel.span = oldPanel.span;
-        row.panels.splice(index, 0, newPanel);
-      });
-    };
-
     $scope.init();
     $scope.init();
-
   });
   });
 
 
   module.directive('rowHeight', function() {
   module.directive('rowHeight', function() {

+ 2 - 2
public/app/features/dashboard/shareModalCtrl.js

@@ -12,7 +12,7 @@ function (angular, _, require, config) {
   module.controller('ShareModalCtrl', function($scope, $rootScope, $location, $timeout, timeSrv, $element, templateSrv, linkSrv) {
   module.controller('ShareModalCtrl', function($scope, $rootScope, $location, $timeout, timeSrv, $element, templateSrv, linkSrv) {
 
 
     $scope.options = { forCurrent: true, includeTemplateVars: true, theme: 'current' };
     $scope.options = { forCurrent: true, includeTemplateVars: true, theme: 'current' };
-    $scope.editor = { index: 0 };
+    $scope.editor = { index: $scope.tabIndex || 0};
 
 
     $scope.init = function() {
     $scope.init = function() {
       $scope.modeSharePanel = $scope.panel ? true : false;
       $scope.modeSharePanel = $scope.panel ? true : false;
@@ -26,7 +26,7 @@ function (angular, _, require, config) {
         $scope.modalTitle = 'Share Dashboard';
         $scope.modalTitle = 'Share Dashboard';
       }
       }
 
 
-      if (!$scope.dashboardMeta.isSnapshot) {
+      if (!$scope.dashboard.meta.isSnapshot) {
         $scope.tabs.push({title: 'Snapshot sharing', src: 'shareSnapshot.html'});
         $scope.tabs.push({title: 'Snapshot sharing', src: 'shareSnapshot.html'});
       }
       }
 
 

+ 1 - 1
public/app/features/dashboard/submenu/submenu.ts

@@ -34,7 +34,7 @@ export class SubmenuCtrl {
 export function submenuDirective() {
 export function submenuDirective() {
   return {
   return {
     restrict: 'E',
     restrict: 'E',
-    templateUrl: 'app/features/dashboard/submenu/submenu.html',
+    templateUrl: 'public/app/features/dashboard/submenu/submenu.html',
     controller: SubmenuCtrl,
     controller: SubmenuCtrl,
     bindToController: true,
     bindToController: true,
     controllerAs: 'ctrl',
     controllerAs: 'ctrl',

+ 0 - 47
public/app/features/dashboard/timepicker/custom.html

@@ -1,47 +0,0 @@
-<div class="gf-box-header">
-	<div class="gf-box-title">
-		<i class="fa fa-clock-o"></i>
-		Custom time range
-	</div>
-	<button class="gf-box-header-close-btn" ng-click="dismiss();">
-		<i class="fa fa-remove"></i>
-	</button>
-</div>
-
-<div class="gf-box-body">
-	<div class="timepicker form-horizontal">
-		<form name="timeForm" style="margin-bottom: 0">
-
-			<div class="timepicker-from-column">
-				<label class="small">From</label>
-				<div class="fake-input timepicker-input">
-					<input class="timepicker-date" type="text" ng-change="validate(temptime)" ng-model="temptime.from.date" data-date-format="yyyy-mm-dd" required bs-datepicker />@
-					<input class="timepicker-hms" type="text" maxlength="2" ng-change="validate(temptime)" ng-model="temptime.from.hour" required ng-pattern="patterns.hour" onClick="this.select();"/>:
-					<input class="timepicker-hms" type="text" maxlength="2" ng-change="validate(temptime)" ng-model="temptime.from.minute" required ng-pattern="patterns.minute" onClick="this.select();"/>:
-					<input class="timepicker-hms" type="text" maxlength="2" ng-change="validate(temptime)" ng-model="temptime.from.second" required ng-pattern="patterns.second" onClick="this.select();"/>.
-					<input class="timepicker-ms" type="text" maxlength="3" ng-change="validate(temptime)" ng-model="temptime.from.millisecond" required ng-pattern="patterns.millisecond"  onClick="this.select();"/>
-				</div>
-			</div>
-
-			<div class="timepicker-to-column">
-
-				<label class="small">To (<a class="link" ng-class="{'strong':temptime.now}" ng-click="ctrl.setNow();temptime.now=true">set now</a>)</label>
-
-				<div class="fake-input timepicker-input">
-					<div ng-hide="temptime.now">
-						<input class="timepicker-date" type="text" ng-change="validate(temptime)" ng-model="temptime.to.date" data-date-format="yyyy-mm-dd" required bs-datepicker />@
-						<input class="timepicker-hms" type="text" maxlength="2" ng-change="validate(temptime)" ng-model="temptime.to.hour" required ng-pattern="patterns.hour" onClick="this.select();"/>:
-						<input class="timepicker-hms" type="text" maxlength="2" ng-change="validate(temptime)" ng-model="temptime.to.minute" required ng-pattern="patterns.minute" onClick="this.select();"/>:
-						<input class="timepicker-hms" type="text" maxlength="2" ng-change="validate(temptime)" ng-model="temptime.to.second" required ng-pattern="patterns.second" onClick="this.select();"/>.
-						<input class="timepicker-ms" type="text" maxlength="3" ng-change="validate(temptime)" ng-model="temptime.to.millisecond" required ng-pattern="patterns.millisecond" onClick="this.select();"/>
-					</div>
-					<span type="text" ng-show="temptime.now" ng-disabled="temptime.now">&nbsp <i class="pointer fa fa-remove" ng-click="ctrl.setNow();temptime.now=false;"></i> Right Now <input type="text" name="dummy" style="visibility:hidden" /></span>
-				</div>
-			</div>
-
-			<br>
-			<button ng-click="ctrl.setAbsoluteTimeFilter(ctrl.validate(temptime));dismiss();" ng-disabled="!timeForm.$valid" class="btn btn-success">Apply</button>
-			<span class="" ng-hide="input.$valid">Invalid date or range</span>
-		</form>
-	</div>
-</div>

+ 3 - 3
public/app/features/dashboard/timepicker/dropdown.html

@@ -1,5 +1,5 @@
 <div class="row pull-right">
 <div class="row pull-right">
-	<div class="gf-timepicker-absolute-section">
+	<form name="timeForm" class="gf-timepicker-absolute-section">
 		<h3>Time range</h3>
 		<h3>Time range</h3>
 		<label class="small">From:</label>
 		<label class="small">From:</label>
 		<div class="input-prepend">
 		<div class="input-prepend">
@@ -29,10 +29,10 @@
 		<select ng-model="ctrl.refresh.value" class='input-medium' ng-options="f.value as f.text for f in ctrl.refresh.options">
 		<select ng-model="ctrl.refresh.value" class='input-medium' ng-options="f.value as f.text for f in ctrl.refresh.options">
 		</select>
 		</select>
 
 
-		<button class="btn btn-inverse gf-timepicker-btn-apply" type="button" ng-click="ctrl.applyCustom()">
+		<button type="submit" class="btn btn-primary" ng-click="ctrl.applyCustom();" ng-disabled="!timeForm.$valid">
 			Apply
 			Apply
 		</button>
 		</button>
-	</div>
+	</form>
 
 
 	<div class="gf-timepicker-relative-section">
 	<div class="gf-timepicker-relative-section">
 		<h3>Quick ranges</h3>
 		<h3>Quick ranges</h3>

+ 14 - 1
public/app/features/dashboard/timepicker/input_date.ts

@@ -1,6 +1,7 @@
 ///<reference path="../../../headers/common.d.ts" />
 ///<reference path="../../../headers/common.d.ts" />
 
 
 import moment from 'moment';
 import moment from 'moment';
+import * as dateMath from 'app/core/utils/datemath';
 
 
 export function inputDateDirective() {
 export function inputDateDirective() {
   return {
   return {
@@ -11,8 +12,14 @@ export function inputDateDirective() {
 
 
       var fromUser = function (text) {
       var fromUser = function (text) {
         if (text.indexOf('now') !== -1) {
         if (text.indexOf('now') !== -1) {
+          if (!dateMath.isValid(text)) {
+            ngModel.$setValidity("error", false);
+            return undefined;
+          }
+          ngModel.$setValidity("error", true);
           return text;
           return text;
         }
         }
+
         var parsed;
         var parsed;
         if ($scope.ctrl.isUtc) {
         if ($scope.ctrl.isUtc) {
           parsed = moment.utc(text, format);
           parsed = moment.utc(text, format);
@@ -20,7 +27,13 @@ export function inputDateDirective() {
           parsed = moment(text, format);
           parsed = moment(text, format);
         }
         }
 
 
-        return parsed.isValid() ? parsed : undefined;
+        if (!parsed.isValid()) {
+          ngModel.$setValidity("error", false);
+          return undefined;
+        }
+
+        ngModel.$setValidity("error", true);
+        return parsed;
       };
       };
 
 
       var toUser = function (currentValue) {
       var toUser = function (currentValue) {

+ 3 - 3
public/app/features/dashboard/timepicker/timepicker.ts

@@ -101,7 +101,7 @@ export class TimePickerCtrl {
     this.refresh.options.unshift({text: 'off'});
     this.refresh.options.unshift({text: 'off'});
 
 
     this.$rootScope.appEvent('show-dash-editor', {
     this.$rootScope.appEvent('show-dash-editor', {
-      src: 'app/features/dashboard/timepicker/dropdown.html',
+      src: 'public/app/features/dashboard/timepicker/dropdown.html',
       scope: this.$scope,
       scope: this.$scope,
       cssClass: 'gf-timepicker-dropdown',
       cssClass: 'gf-timepicker-dropdown',
     });
     });
@@ -146,7 +146,7 @@ export class TimePickerCtrl {
 export function settingsDirective() {
 export function settingsDirective() {
   return {
   return {
     restrict: 'E',
     restrict: 'E',
-    templateUrl: 'app/features/dashboard/timepicker/settings.html',
+    templateUrl: 'public/app/features/dashboard/timepicker/settings.html',
     controller: TimePickerCtrl,
     controller: TimePickerCtrl,
     bindToController: true,
     bindToController: true,
     controllerAs: 'ctrl',
     controllerAs: 'ctrl',
@@ -159,7 +159,7 @@ export function settingsDirective() {
 export function timePickerDirective() {
 export function timePickerDirective() {
   return {
   return {
     restrict: 'E',
     restrict: 'E',
-    templateUrl: 'app/features/dashboard/timepicker/timepicker.html',
+    templateUrl: 'public/app/features/dashboard/timepicker/timepicker.html',
     controller: TimePickerCtrl,
     controller: TimePickerCtrl,
     bindToController: true,
     bindToController: true,
     controllerAs: 'ctrl',
     controllerAs: 'ctrl',

+ 1 - 1
public/app/features/dashboard/unsavedChangesSrv.js

@@ -139,7 +139,7 @@ function(angular, _) {
       };
       };
 
 
       $rootScope.appEvent('show-modal', {
       $rootScope.appEvent('show-modal', {
-        src: './app/partials/unsaved-changes.html',
+        src: 'public/app/partials/unsaved-changes.html',
         modalClass: 'modal-no-header confirm-modal',
         modalClass: 'modal-no-header confirm-modal',
         scope: modalScope,
         scope: modalScope,
       });
       });

+ 30 - 15
public/app/features/dashboard/viewStateSrv.js

@@ -17,6 +17,7 @@ function (angular, _, $) {
       self.state = {};
       self.state = {};
       self.panelScopes = [];
       self.panelScopes = [];
       self.$scope = $scope;
       self.$scope = $scope;
+      self.dashboard = $scope.dashboard;
 
 
       $scope.exitFullscreen = function() {
       $scope.exitFullscreen = function() {
         if (self.state.fullscreen) {
         if (self.state.fullscreen) {
@@ -31,6 +32,14 @@ function (angular, _, $) {
         }
         }
       });
       });
 
 
+      $scope.onAppEvent('panel-change-view', function(evt, payload) {
+        self.update(payload);
+      });
+
+      $scope.onAppEvent('panel-instantiated', function(evt, payload) {
+        self.registerPanel(payload.scope);
+      });
+
       this.update(this.getQueryStringState(), true);
       this.update(this.getQueryStringState(), true);
       this.expandRowForPanel();
       this.expandRowForPanel();
     }
     }
@@ -66,7 +75,7 @@ function (angular, _, $) {
 
 
     DashboardViewState.prototype.update = function(state, skipUrlSync) {
     DashboardViewState.prototype.update = function(state, skipUrlSync) {
       _.extend(this.state, state);
       _.extend(this.state, state);
-      this.fullscreen = this.state.fullscreen;
+      this.dashboard.meta.fullscreen = this.state.fullscreen;
 
 
       if (!this.state.fullscreen) {
       if (!this.state.fullscreen) {
         this.state.panelId = null;
         this.state.panelId = null;
@@ -84,7 +93,7 @@ function (angular, _, $) {
     DashboardViewState.prototype.syncState = function() {
     DashboardViewState.prototype.syncState = function() {
       if (this.panelScopes.length === 0) { return; }
       if (this.panelScopes.length === 0) { return; }
 
 
-      if (this.fullscreen) {
+      if (this.dashboard.meta.fullscreen) {
         if (this.fullscreenPanel) {
         if (this.fullscreenPanel) {
           this.leaveFullscreen(false);
           this.leaveFullscreen(false);
         }
         }
@@ -105,23 +114,24 @@ function (angular, _, $) {
 
 
     DashboardViewState.prototype.getPanelScope = function(id) {
     DashboardViewState.prototype.getPanelScope = function(id) {
       return _.find(this.panelScopes, function(panelScope) {
       return _.find(this.panelScopes, function(panelScope) {
-        return panelScope.panel.id === id;
+        return panelScope.ctrl.panel.id === id;
       });
       });
     };
     };
 
 
     DashboardViewState.prototype.leaveFullscreen = function(render) {
     DashboardViewState.prototype.leaveFullscreen = function(render) {
       var self = this;
       var self = this;
+      var ctrl = self.fullscreenPanel.ctrl;
 
 
-      self.fullscreenPanel.editMode = false;
-      self.fullscreenPanel.fullscreen = false;
-      delete self.fullscreenPanel.height;
+      ctrl.editMode = false;
+      ctrl.fullscreen = false;
+      delete ctrl.height;
 
 
-      this.$scope.appEvent('panel-fullscreen-exit', {panelId: this.fullscreenPanel.panel.id});
+      this.$scope.appEvent('panel-fullscreen-exit', {panelId: ctrl.panel.id});
 
 
       if (!render) { return false;}
       if (!render) { return false;}
 
 
       $timeout(function() {
       $timeout(function() {
-        if (self.oldTimeRange !== self.fullscreenPanel.range) {
+        if (self.oldTimeRange !== ctrl.range) {
           self.$scope.broadcastRefresh();
           self.$scope.broadcastRefresh();
         }
         }
         else {
         else {
@@ -135,17 +145,18 @@ function (angular, _, $) {
       var docHeight = $(window).height();
       var docHeight = $(window).height();
       var editHeight = Math.floor(docHeight * 0.3);
       var editHeight = Math.floor(docHeight * 0.3);
       var fullscreenHeight = Math.floor(docHeight * 0.7);
       var fullscreenHeight = Math.floor(docHeight * 0.7);
+      var ctrl = panelScope.ctrl;
 
 
-      panelScope.editMode = this.state.edit && this.$scope.dashboardMeta.canEdit;
-      panelScope.height = panelScope.editMode ? editHeight : fullscreenHeight;
+      ctrl.editMode = this.state.edit && this.$scope.dashboardMeta.canEdit;
+      ctrl.height = ctrl.editMode ? editHeight : fullscreenHeight;
+      ctrl.fullscreen = true;
 
 
-      this.oldTimeRange = panelScope.range;
+      this.oldTimeRange = ctrl.range;
       this.fullscreenPanel = panelScope;
       this.fullscreenPanel = panelScope;
 
 
       $(window).scrollTop(0);
       $(window).scrollTop(0);
 
 
-      panelScope.fullscreen = true;
-      this.$scope.appEvent('panel-fullscreen-enter', {panelId: panelScope.panel.id});
+      this.$scope.appEvent('panel-fullscreen-enter', {panelId: ctrl.panel.id});
 
 
       $timeout(function() {
       $timeout(function() {
         panelScope.$broadcast('render');
         panelScope.$broadcast('render');
@@ -156,8 +167,12 @@ function (angular, _, $) {
       var self = this;
       var self = this;
       self.panelScopes.push(panelScope);
       self.panelScopes.push(panelScope);
 
 
-      if (self.state.panelId === panelScope.panel.id) {
-        self.enterFullscreen(panelScope);
+      if (self.state.panelId === panelScope.ctrl.panel.id) {
+        if (self.state.edit) {
+          panelScope.ctrl.editPanel();
+        } else {
+          panelScope.ctrl.viewPanel();
+        }
       }
       }
 
 
       panelScope.$on('$destroy', function() {
       panelScope.$on('$destroy', function() {

+ 1 - 1
public/app/features/dashlinks/module.js

@@ -21,7 +21,7 @@ function (angular, _) {
     return {
     return {
       restrict: 'E',
       restrict: 'E',
       controller: 'DashLinkEditorCtrl',
       controller: 'DashLinkEditorCtrl',
-      templateUrl: 'app/features/dashlinks/editor.html',
+      templateUrl: 'public/app/features/dashlinks/editor.html',
       link: function() {
       link: function() {
       }
       }
     };
     };

+ 1 - 1
public/app/features/datasources/edit_ctrl.js

@@ -10,7 +10,7 @@ function (angular, _, config) {
   var datasourceTypes = [];
   var datasourceTypes = [];
 
 
   module.directive('datasourceHttpSettings', function() {
   module.directive('datasourceHttpSettings', function() {
-    return {templateUrl: 'app/features/datasources/partials/http_settings.html'};
+    return {templateUrl: 'public/app/features/datasources/partials/http_settings.html'};
   });
   });
 
 
   module.controller('DataSourceEditCtrl', function($scope, $q, backendSrv, $routeParams, $location, datasourceSrv) {
   module.controller('DataSourceEditCtrl', function($scope, $q, backendSrv, $routeParams, $location, datasourceSrv) {

+ 2 - 2
public/app/features/datasources/partials/edit.html

@@ -1,9 +1,9 @@
-<topnav title="Data sources" title-url="datasources" icon="fa fa-fw fa-database" subnav="true">
+<navbar title="Data sources" title-url="datasources" icon="fa fa-fw fa-database" subnav="true">
 	<ul class="nav">
 	<ul class="nav">
 		<li ng-class="{active: isNew}" ng-show="isNew"><a href="datasources/new">Add new</a></li>
 		<li ng-class="{active: isNew}" ng-show="isNew"><a href="datasources/new">Add new</a></li>
 		<li class="active" ng-show="!isNew"><a href="datasources/edit/{{current.name}}">{{current.name}}</a></li>
 		<li class="active" ng-show="!isNew"><a href="datasources/edit/{{current.name}}">{{current.name}}</a></li>
 	</ul>
 	</ul>
-</topnav>
+</navbar>
 
 
 <div class="page-container">
 <div class="page-container">
 	<div class="page">
 	<div class="page">

+ 3 - 3
public/app/features/datasources/partials/list.html

@@ -1,10 +1,10 @@
-<topnav title="Data sources" icon="fa fa-fw fa-database" subnav="false">
-</topnav>
+<navbar title="Data sources" icon="fa fa-fw fa-database">
+</navbar>
 
 
 <div class="page-container">
 <div class="page-container">
 	<div class="page-wide">
 	<div class="page-wide">
 
 
-		<a type="submit" class="btn btn-inverse pull-right" href="datasources/new">
+		<a class="btn btn-inverse pull-right" href="datasources/new">
 			<i class="fa fa-plus"></i>
 			<i class="fa fa-plus"></i>
 			Add data source
 			Add data source
 		</a>
 		</a>

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

@@ -32,7 +32,7 @@ function (angular) {
         modalScope.key = result.key;
         modalScope.key = result.key;
 
 
         $scope.appEvent('show-modal', {
         $scope.appEvent('show-modal', {
-          src: './app/features/org/partials/apikeyModal.html',
+          src: 'public/app/features/org/partials/apikeyModal.html',
           scope: modalScope
           scope: modalScope
         });
         });
 
 

+ 1 - 1
public/app/features/org/orgUsersCtrl.js

@@ -54,7 +54,7 @@ function (angular) {
       };
       };
 
 
       $scope.appEvent('show-modal', {
       $scope.appEvent('show-modal', {
-        src: './app/features/org/partials/invite.html',
+        src: 'public/app/features/org/partials/invite.html',
         modalClass: 'modal-no-header invite-modal',
         modalClass: 'modal-no-header invite-modal',
         scope: modalScope
         scope: modalScope
       });
       });

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

@@ -1,8 +1,8 @@
-<topnav title="Organization" icon="fa fa-fw fa-users" subnav="true">
+<navbar title="Organization" icon="fa fa-fw fa-users" subnav="true">
 	<ul class="nav">
 	<ul class="nav">
 		<li class="active"><a href="org/new">New organization</a></li>
 		<li class="active"><a href="org/new">New organization</a></li>
 	</ul>
 	</ul>
-</topnav>
+</navbar>
 
 
 <div class="page-container">
 <div class="page-container">
 	<div class="page">
 	<div class="page">

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

@@ -1,8 +1,8 @@
-<topnav icon="fa fa-fw fa-users" title="Organization" subnav="true">
+<navbar icon="fa fa-fw fa-users" title="Organization" subnav="true">
 	<ul class="nav">
 	<ul class="nav">
 		<li class="active"><a href="org/apikeys">API Keys</a></li>
 		<li class="active"><a href="org/apikeys">API Keys</a></li>
 	</ul>
 	</ul>
-</topnav>
+</navbar>
 
 
 <div class="page-container">
 <div class="page-container">
 	<div class="page-wide">
 	<div class="page-wide">

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

@@ -1,8 +1,8 @@
-<topnav icon="fa fa-fw fa-users" title="Organization" subnav="true">
+<navbar icon="fa fa-fw fa-users" title="Organization">
 	<ul class="nav">
 	<ul class="nav">
 		<li class="active"><a href="org">Preferences</a></li>
 		<li class="active"><a href="org">Preferences</a></li>
 	</ul>
 	</ul>
-</topnav>
+</navbar>
 
 
 <div class="page-container">
 <div class="page-container">
 	<div class="page">
 	<div class="page">

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

@@ -1,8 +1,8 @@
-<topnav title="Organization" icon="fa fa-fw fa-users" subnav="true">
+<navbar title="Organization" icon="fa fa-fw fa-users" subnav="true">
 	<ul class="nav">
 	<ul class="nav">
 		<li class="active"><a href="org/users">Users</a></li>
 		<li class="active"><a href="org/users">Users</a></li>
 	</ul>
 	</ul>
-</topnav>
+</navbar>
 
 
 <div class="page-container">
 <div class="page-container">
 	<div class="page-wide">
 	<div class="page-wide">

+ 1 - 2
public/app/features/panel/all.js

@@ -1,9 +1,8 @@
 define([
 define([
   './panel_menu',
   './panel_menu',
   './panel_directive',
   './panel_directive',
-  './panel_srv',
-  './panel_helper',
   './solo_panel_ctrl',
   './solo_panel_ctrl',
   './panel_loader',
   './panel_loader',
   './query_editor',
   './query_editor',
+  './panel_editor_tab',
 ], function () {});
 ], function () {});

+ 234 - 0
public/app/features/panel/metrics_panel_ctrl.ts

@@ -0,0 +1,234 @@
+///<reference path="../../headers/common.d.ts" />
+
+import config from 'app/core/config';
+import $ from 'jquery';
+import _ from 'lodash';
+import kbn from 'app/core/utils/kbn';
+import {PanelCtrl} from './panel_ctrl';
+
+import * as rangeUtil from '../../core/utils/rangeutil';
+import * as dateMath from '../../core/utils/datemath';
+
+class MetricsPanelCtrl extends PanelCtrl {
+  error: boolean;
+  loading: boolean;
+  datasource: any;
+  $q: any;
+  $timeout: any;
+  datasourceSrv: any;
+  timeSrv: any;
+  timing: any;
+  range: any;
+  rangeRaw: any;
+  interval: any;
+  resolution: any;
+  timeInfo: any;
+  skipDataOnInit: boolean;
+  datasources: any[];
+
+  constructor($scope, $injector) {
+    super($scope, $injector);
+
+    // make metrics tab the default
+    this.editorTabIndex = 1;
+    this.$q = $injector.get('$q');
+    this.datasourceSrv = $injector.get('datasourceSrv');
+    this.timeSrv = $injector.get('timeSrv');
+
+    if (!this.panel.targets) {
+      this.panel.targets = [{}];
+    }
+
+    // hookup initial data fetch
+    this.$timeout(() => {
+      if (!this.skipDataOnInit) {
+        this.refresh();
+      }
+    }, 30);;
+  }
+
+  initEditMode() {
+    this.addEditorTab('Metrics', 'public/app/partials/metrics.html');
+    this.addEditorTab('Time range', 'public/app/features/panel/partials/panelTime.html');
+    this.datasources = this.datasourceSrv.getMetricSources();
+  }
+
+  refresh() {
+    this.getData();
+  }
+
+  refreshData(data) {
+    // null op
+    return this.$q.when(data);
+  }
+
+  loadSnapshot(data) {
+    // null op
+    return data;
+  }
+
+  getData() {
+    // ignore fetching data if another panel is in fullscreen
+    if (this.otherPanelInFullscreenMode()) { return; }
+
+    // if we have snapshot data use that
+    if (this.panel.snapshotData) {
+      if (this.loadSnapshot) {
+        this.loadSnapshot(this.panel.snapshotData);
+      }
+      return;
+    }
+
+    // clear loading/error state
+    delete this.error;
+    this.loading = true;
+
+    // load datasource service
+    this.datasourceSrv.get(this.panel.datasource).then(datasource => {
+      this.datasource = datasource;
+      return this.refreshData(this.datasource);
+    }).then(() => {
+      this.loading = false;
+    }).catch(err => {
+      console.log('Panel data error:', err);
+      this.loading = false;
+      this.error = err.message || "Timeseries data request error";
+      this.inspector = {error: err};
+    });
+  }
+
+  setTimeQueryStart() {
+    this.timing = {};
+    this.timing.queryStart = new Date().getTime();
+  }
+
+  setTimeQueryEnd() {
+    this.timing.queryEnd = new Date().getTime();
+  }
+
+  updateTimeRange() {
+    this.range = this.timeSrv.timeRange();
+    this.rangeRaw = this.timeSrv.timeRange(false);
+
+    this.applyPanelTimeOverrides();
+
+    if (this.panel.maxDataPoints) {
+      this.resolution = this.panel.maxDataPoints;
+    } else {
+      this.resolution = Math.ceil($(window).width() * (this.panel.span / 12));
+    }
+
+    var panelInterval = this.panel.interval;
+    var datasourceInterval = (this.datasource || {}).interval;
+      this.interval = kbn.calculateInterval(this.range, this.resolution, panelInterval || datasourceInterval);
+    };
+
+    applyPanelTimeOverrides() {
+      this.timeInfo = '';
+
+      // check panel time overrrides
+      if (this.panel.timeFrom) {
+        var timeFromInfo = rangeUtil.describeTextRange(this.panel.timeFrom);
+        if (timeFromInfo.invalid) {
+          this.timeInfo = 'invalid time override';
+          return;
+        }
+
+        if (_.isString(this.rangeRaw.from)) {
+          var timeFromDate = dateMath.parse(timeFromInfo.from);
+          this.timeInfo = timeFromInfo.display;
+          this.rangeRaw.from = timeFromInfo.from;
+          this.rangeRaw.to = timeFromInfo.to;
+          this.range.from = timeFromDate;
+        }
+      }
+
+      if (this.panel.timeShift) {
+        var timeShiftInfo = rangeUtil.describeTextRange(this.panel.timeShift);
+        if (timeShiftInfo.invalid) {
+          this.timeInfo = 'invalid timeshift';
+          return;
+        }
+
+        var timeShift = '-' + this.panel.timeShift;
+        this.timeInfo += ' timeshift ' + timeShift;
+        this.range.from = dateMath.parseDateMath(timeShift, this.range.from, false);
+        this.range.to = dateMath.parseDateMath(timeShift, this.range.to, true);
+
+        this.rangeRaw = this.range;
+      }
+
+      if (this.panel.hideTimeOverride) {
+        this.timeInfo = '';
+      }
+    };
+
+  issueQueries(datasource) {
+    if (!this.panel.targets || this.panel.targets.length === 0) {
+      return this.$q.when([]);
+    }
+
+    this.updateTimeRange();
+
+    var metricsQuery = {
+      range: this.range,
+      rangeRaw: this.rangeRaw,
+      interval: this.interval,
+      targets: this.panel.targets,
+      format: this.panel.renderer === 'png' ? 'png' : 'json',
+      maxDataPoints: this.resolution,
+      scopedVars: this.panel.scopedVars,
+      cacheTimeout: this.panel.cacheTimeout
+    };
+
+    this.setTimeQueryStart();
+    return datasource.query(metricsQuery).then(results => {
+      this.setTimeQueryEnd();
+
+      if (this.dashboard.snapshot) {
+        this.panel.snapshotData = results;
+      }
+
+      return results;
+    });
+  }
+
+  addDataQuery(datasource) {
+    this.dashboard.addDataQueryTo(this.panel, datasource);
+  }
+
+  removeDataQuery(query) {
+    this.dashboard.removeDataQuery(this.panel, query);
+    this.refresh();
+  };
+
+  duplicateDataQuery(query) {
+    this.dashboard.duplicateDataQuery(this.panel, query);
+  }
+
+  moveDataQuery(fromIndex, toIndex) {
+    this.dashboard.moveDataQuery(this.panel, fromIndex, toIndex);
+  }
+
+  setDatasource(datasource) {
+    // switching to mixed
+    if (datasource.meta.mixed) {
+      _.each(this.panel.targets, target => {
+        target.datasource = this.panel.datasource;
+        if (target.datasource === null) {
+          target.datasource = config.defaultDatasource;
+        }
+      });
+    } else if (this.datasource && this.datasource.meta.mixed) {
+      _.each(this.panel.targets, target => {
+        delete target.datasource;
+      });
+    }
+
+    this.panel.datasource = datasource.value;
+    this.datasource = null;
+    this.refresh();
+  }
+}
+
+export {MetricsPanelCtrl};

+ 51 - 0
public/app/features/panel/panel.ts

@@ -0,0 +1,51 @@
+///<reference path="../../headers/common.d.ts" />
+
+import config from 'app/core/config';
+
+import {PanelCtrl} from './panel_ctrl';
+import {MetricsPanelCtrl} from './metrics_panel_ctrl';
+
+export class DefaultPanelCtrl extends PanelCtrl {
+  /** @ngInject */
+  constructor($scope, $injector) {
+    super($scope, $injector);
+  }
+}
+
+class PanelDirective {
+  template: string;
+  templateUrl: string;
+  bindToController: boolean;
+  scope: any;
+  controller: any;
+  controllerAs: string;
+
+  getDirective() {
+    if (!this.controller) {
+      this.controller = DefaultPanelCtrl;
+    }
+
+    return {
+      template: this.template,
+      templateUrl: this.templateUrl,
+      controller: this.controller,
+      controllerAs: 'ctrl',
+      bindToController: true,
+      scope: {dashboard: "=", panel: "=", row: "="},
+      link: (scope, elem, attrs, ctrl) => {
+        ctrl.init();
+        this.link(scope, elem, attrs, ctrl);
+      }
+    };
+  }
+
+  link(scope, elem, attrs, ctrl) {
+    return null;
+  }
+}
+
+export {
+  PanelCtrl,
+  MetricsPanelCtrl,
+  PanelDirective,
+}

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

@@ -0,0 +1,179 @@
+///<reference path="../../headers/common.d.ts" />
+
+import config from 'app/core/config';
+import _ from 'lodash';
+
+export class PanelCtrl {
+  panel: any;
+  row: any;
+  dashboard: any;
+  editorTabIndex: number;
+  pluginName: string;
+  pluginId: string;
+  icon: string;
+  editorTabs: any;
+  $scope: any;
+  $injector: any;
+  $timeout: any;
+  fullscreen: boolean;
+  inspector: any;
+  editModeInitiated: boolean;
+  editorHelpIndex: number;
+
+  constructor($scope, $injector) {
+    this.$injector = $injector;
+    this.$scope = $scope;
+    this.$timeout = $injector.get('$timeout');
+    this.editorTabIndex = 0;
+
+    var plugin = config.panels[this.panel.type];
+    if (plugin) {
+      this.pluginId = plugin.id;
+      this.pluginName = plugin.name;
+    }
+
+    $scope.$on("refresh", () => this.refresh());
+  }
+
+  init() {
+    this.publishAppEvent('panel-instantiated', {scope: this.$scope});
+    this.refresh();
+  }
+
+  renderingCompleted() {
+    this.$scope.$root.performance.panelsRendered++;
+  }
+
+  refresh() {
+    return;
+  }
+
+  publishAppEvent(evtName, evt) {
+    this.$scope.$root.appEvent(evtName, evt);
+  }
+
+  changeView(fullscreen, edit) {
+    this.publishAppEvent('panel-change-view', {
+      fullscreen: fullscreen, edit: edit, panelId: this.panel.id
+    });
+  }
+
+  viewPanel() {
+    this.changeView(true, false);
+  }
+
+  editPanel() {
+    if (!this.editModeInitiated) {
+      this.editorTabs = [];
+      this.addEditorTab('General', 'public/app/partials/panelgeneral.html');
+      this.initEditMode();
+    }
+
+    this.changeView(true, true);
+  }
+
+  exitFullscreen() {
+    this.changeView(false, false);
+  }
+
+  initEditMode() {
+    return;
+  }
+
+  addEditorTab(title, directiveFn, index?) {
+    var editorTab = {title, directiveFn};
+
+    if (_.isString(directiveFn)) {
+      editorTab.directiveFn = function() {
+        return {templateUrl: directiveFn};
+      };
+    }
+    if (index) {
+      this.editorTabs.splice(index, 0, editorTab);
+    } else {
+      this.editorTabs.push(editorTab);
+    }
+  }
+
+  getMenu() {
+    let menu = [];
+    menu.push({text: 'View', click: 'ctrl.viewPanel(); dismiss();'});
+    menu.push({text: 'Edit', click: 'ctrl.editPanel(); dismiss();', role: 'Editor'});
+    menu.push({text: 'Duplicate', click: 'ctrl.duplicate()', role: 'Editor' });
+    menu.push({text: 'Share', click: 'ctrl.sharePanel(); dismiss();'});
+    return menu;
+  }
+
+  getExtendedMenu() {
+    return [{text: 'Panel JSON', click: 'ctrl.editPanelJson(); dismiss();'}];
+  }
+
+  otherPanelInFullscreenMode() {
+    return this.dashboard.meta.fullscreen && !this.fullscreen;
+  }
+
+  broadcastRender(arg1?, arg2?) {
+    this.$scope.$broadcast('render', arg1, arg2);
+  }
+
+  toggleEditorHelp(index) {
+    if (this.editorHelpIndex === index) {
+      this.editorHelpIndex = null;
+      return;
+    }
+    this.editorHelpIndex = index;
+  }
+
+  duplicate() {
+    this.dashboard.duplicatePanel(this.panel, this.row);
+  }
+
+  updateColumnSpan(span) {
+    this.panel.span = Math.min(Math.max(Math.floor(this.panel.span + span), 1), 12);
+    this.$timeout(() => {
+      this.broadcastRender();
+    });
+  }
+
+  removePanel() {
+    this.publishAppEvent('confirm-modal', {
+      title: 'Are you sure you want to remove this panel?',
+      icon: 'fa-trash',
+      yesText: 'Delete',
+      onConfirm: () => {
+        this.row.panels = _.without(this.row.panels, this.panel);
+      }
+    });
+  }
+
+  editPanelJson() {
+    this.publishAppEvent('show-json-editor', {
+      object: this.panel,
+      updateHandler: this.replacePanel.bind(this)
+    });
+  }
+
+  replacePanel(newPanel, oldPanel) {
+    var row = this.row;
+    var index = _.indexOf(this.row.panels, oldPanel);
+    this.row.panels.splice(index, 1);
+
+    // adding it back needs to be done in next digest
+    this.$timeout(() => {
+      newPanel.id = oldPanel.id;
+      newPanel.span = oldPanel.span;
+      this.row.panels.splice(index, 0, newPanel);
+    });
+  }
+
+ sharePanel() {
+   var shareScope = this.$scope.$new();
+   shareScope.panel = this.panel;
+   shareScope.dashboard = this.dashboard;
+
+   this.publishAppEvent('show-modal', {
+     src: 'public/app/features/dashboard/partials/shareModal.html',
+     scope: shareScope
+   });
+ }
+}

+ 17 - 15
public/app/features/panel/panel_directive.js

@@ -10,14 +10,15 @@ function (angular, $) {
   module.directive('grafanaPanel', function() {
   module.directive('grafanaPanel', function() {
     return {
     return {
       restrict: 'E',
       restrict: 'E',
-      templateUrl: 'app/features/panel/partials/panel.html',
+      templateUrl: 'public/app/features/panel/partials/panel.html',
       transclude: true,
       transclude: true,
+      scope: { ctrl: "=" },
       link: function(scope, elem) {
       link: function(scope, elem) {
         var panelContainer = elem.find('.panel-container');
         var panelContainer = elem.find('.panel-container');
-
-        scope.$watchGroup(['fullscreen', 'height', 'panel.height', 'row.height'], function() {
-          panelContainer.css({ minHeight: scope.height || scope.panel.height || scope.row.height, display: 'block' });
-          elem.toggleClass('panel-fullscreen', scope.fullscreen ? true : false);
+        var ctrl = scope.ctrl;
+        scope.$watchGroup(['ctrl.fullscreen', 'ctrl.height', 'ctrl.panel.height', 'ctrl.row.height'], function() {
+          panelContainer.css({ minHeight: ctrl.height || ctrl.panel.height || ctrl.row.height, display: 'block' });
+          elem.toggleClass('panel-fullscreen', ctrl.fullscreen ? true : false);
         });
         });
       }
       }
     };
     };
@@ -30,6 +31,7 @@ function (angular, $) {
       link: function(scope, elem) {
       link: function(scope, elem) {
         var resizing = false;
         var resizing = false;
         var lastPanel = false;
         var lastPanel = false;
+        var ctrl = scope.ctrl;
         var handleOffset;
         var handleOffset;
         var originalHeight;
         var originalHeight;
         var originalWidth;
         var originalWidth;
@@ -40,31 +42,31 @@ function (angular, $) {
           resizing = true;
           resizing = true;
 
 
           handleOffset = $(e.target).offset();
           handleOffset = $(e.target).offset();
-          originalHeight = parseInt(scope.row.height);
-          originalWidth = scope.panel.span;
+          originalHeight = parseInt(ctrl.row.height);
+          originalWidth = ctrl.panel.span;
           maxWidth = $(document).width();
           maxWidth = $(document).width();
 
 
-          lastPanel = scope.row.panels[scope.row.panels.length - 1];
+          lastPanel = ctrl.row.panels[ctrl.row.panels.length - 1];
 
 
           $('body').on('mousemove', moveHandler);
           $('body').on('mousemove', moveHandler);
           $('body').on('mouseup', dragEndHandler);
           $('body').on('mouseup', dragEndHandler);
         }
         }
 
 
         function moveHandler(e) {
         function moveHandler(e) {
-          scope.row.height = originalHeight + (e.pageY - handleOffset.top);
-          scope.panel.span = originalWidth + (((e.pageX - handleOffset.left) / maxWidth) * 12);
-          scope.panel.span = Math.min(Math.max(scope.panel.span, 1), 12);
+          ctrl.row.height = originalHeight + (e.pageY - handleOffset.top);
+          ctrl.panel.span = originalWidth + (((e.pageX - handleOffset.left) / maxWidth) * 12);
+          ctrl.panel.span = Math.min(Math.max(ctrl.panel.span, 1), 12);
 
 
-          var rowSpan = scope.dashboard.rowSpan(scope.row);
+          var rowSpan = ctrl.dashboard.rowSpan(ctrl.row);
 
 
           // auto adjust other panels
           // auto adjust other panels
           if (Math.floor(rowSpan) < 14) {
           if (Math.floor(rowSpan) < 14) {
             // last panel should not push row down
             // last panel should not push row down
-            if (lastPanel === scope.panel && rowSpan > 12) {
+            if (lastPanel === ctrl.panel && rowSpan > 12) {
               lastPanel.span -= rowSpan - 12;
               lastPanel.span -= rowSpan - 12;
             }
             }
             // reduce width of last panel so total in row is 12
             // reduce width of last panel so total in row is 12
-            else if (lastPanel !== scope.panel) {
+            else if (lastPanel !== ctrl.panel) {
               lastPanel.span = lastPanel.span - (rowSpan - 12);
               lastPanel.span = lastPanel.span - (rowSpan - 12);
               lastPanel.span = Math.min(Math.max(lastPanel.span, 1), 12);
               lastPanel.span = Math.min(Math.max(lastPanel.span, 1), 12);
             }
             }
@@ -77,7 +79,7 @@ function (angular, $) {
 
 
         function dragEndHandler() {
         function dragEndHandler() {
           // if close to 12
           // if close to 12
-          var rowSpan = scope.dashboard.rowSpan(scope.row);
+          var rowSpan = ctrl.dashboard.rowSpan(ctrl.row);
           if (rowSpan < 12 && rowSpan > 11) {
           if (rowSpan < 12 && rowSpan > 11) {
             lastPanel.span +=  12 - rowSpan;
             lastPanel.span +=  12 - rowSpan;
           }
           }

+ 28 - 0
public/app/features/panel/panel_editor_tab.ts

@@ -0,0 +1,28 @@
+///<reference path="../../headers/common.d.ts" />
+
+import angular from 'angular';
+import config from 'app/core/config';
+
+var directiveModule = angular.module('grafana.directives');
+
+/** @ngInject */
+function panelEditorTab(dynamicDirectiveSrv) {
+  return dynamicDirectiveSrv.create({
+    scope: {
+      ctrl: "=",
+      editorTab: "=",
+      index: "=",
+    },
+    directive: scope => {
+      var pluginId = scope.ctrl.pluginId;
+      var tabIndex = scope.index;
+
+      return Promise.resolve({
+        name: `panel-editor-tab-${pluginId}${tabIndex}`,
+        fn: scope.editorTab.directiveFn,
+      });
+    }
+  });
+}
+
+directiveModule.directive('panelEditorTab', panelEditorTab);

+ 74 - 16
public/app/features/panel/panel_loader.ts

@@ -3,28 +3,86 @@
 import angular from 'angular';
 import angular from 'angular';
 import config from 'app/core/config';
 import config from 'app/core/config';
 
 
-import {unknownPanelDirective} from '../../plugins/panel/unknown/module';
+import {UnknownPanel} from '../../plugins/panel/unknown/module';
+
+var directiveModule = angular.module('grafana.directives');
 
 
 /** @ngInject */
 /** @ngInject */
-function panelLoader($parse, dynamicDirectiveSrv) {
-  return dynamicDirectiveSrv.create({
-    directive: scope => {
+function panelLoader($compile, dynamicDirectiveSrv, $http, $q, $injector, $templateCache) {
+  return {
+    restrict: 'E',
+    scope: {
+      dashboard: "=",
+      row: "=",
+      panel: "="
+    },
+    link: function(scope, elem, attrs) {
+
+      function getTemplate(directive) {
+        if (directive.template) {
+          return $q.when(directive.template);
+        }
+        var cached = $templateCache.get(directive.templateUrl);
+        if (cached) {
+          return $q.when(cached);
+        }
+        return $http.get(directive.templateUrl).then(res => {
+          return res.data;
+        });
+      }
+
+      function addPanelAndCompile(name) {
+        var child = angular.element(document.createElement(name));
+        child.attr('dashboard', 'dashboard');
+        child.attr('panel', 'panel');
+        child.attr('row', 'row');
+        $compile(child)(scope);
+
+        elem.empty();
+        elem.append(child);
+      }
+
+      function addPanel(name, Panel) {
+        if (Panel.registered) {
+          addPanelAndCompile(name);
+          return;
+        }
+
+        if (Panel.promise) {
+          Panel.promise.then(() => {
+            addPanelAndCompile(name);
+          });
+          return;
+        }
+
+        var panelInstance = $injector.instantiate(Panel);
+        var directive = panelInstance.getDirective();
+
+        Panel.promise = getTemplate(directive).then(template => {
+          directive.templateUrl = null;
+          directive.template = `<grafana-panel ctrl="ctrl">${template}</grafana-panel>`;
+          directiveModule.directive(attrs.$normalize(name), function() {
+            return directive;
+          });
+          Panel.registered = true;
+          addPanelAndCompile(name);
+        });
+      }
+
+      var panelElemName = 'panel-directive-' + scope.panel.type;
       let panelInfo = config.panels[scope.panel.type];
       let panelInfo = config.panels[scope.panel.type];
       if (!panelInfo) {
       if (!panelInfo) {
-        return Promise.resolve({
-          name: 'panel-directive-' + scope.panel.type,
-          fn: unknownPanelDirective
-        });
+        addPanel(panelElemName, UnknownPanel);
+        return;
       }
       }
 
 
-      return System.import(panelInfo.module).then(function(panelModule) {
-        return {
-          name: 'panel-directive-' + scope.panel.type,
-          fn: panelModule.panel,
-        };
+      System.import(panelInfo.module).then(function(panelModule) {
+        addPanel(panelElemName, panelModule.Panel);
+      }).catch(err => {
+        console.log('Panel err: ', err);
       });
       });
-    },
-  });
+    }
+  };
 }
 }
 
 
-angular.module('grafana.directives').directive('panelLoader', panelLoader);
+directiveModule.directive('panelLoader', panelLoader);

+ 23 - 21
public/app/features/panel/panel_menu.js

@@ -11,32 +11,33 @@ function (angular, $, _) {
     .directive('panelMenu', function($compile, linkSrv) {
     .directive('panelMenu', function($compile, linkSrv) {
       var linkTemplate =
       var linkTemplate =
           '<span class="panel-title drag-handle pointer">' +
           '<span class="panel-title drag-handle pointer">' +
-            '<span class="panel-title-text drag-handle">{{panel.title | interpolateTemplateVars:this}}</span>' +
+            '<span class="panel-title-text drag-handle">{{ctrl.panel.title | interpolateTemplateVars:this}}</span>' +
             '<span class="panel-links-btn"><i class="fa fa-external-link"></i></span>' +
             '<span class="panel-links-btn"><i class="fa fa-external-link"></i></span>' +
-            '<span class="panel-time-info" ng-show="panelMeta.timeInfo"><i class="fa fa-clock-o"></i> {{panelMeta.timeInfo}}</span>' +
+            '<span class="panel-time-info" ng-show="ctrl.timeInfo"><i class="fa fa-clock-o"></i> {{ctrl.timeInfo}}</span>' +
           '</span>';
           '</span>';
 
 
-      function createExternalLinkMenu($scope) {
+      function createExternalLinkMenu(ctrl) {
         var template = '<div class="panel-menu small">';
         var template = '<div class="panel-menu small">';
         template += '<div class="panel-menu-row">';
         template += '<div class="panel-menu-row">';
 
 
-        if ($scope.panel.links) {
-          _.each($scope.panel.links, function(link) {
-            var info = linkSrv.getPanelLinkAnchorInfo(link, $scope.panel.scopedVars);
+        if (ctrl.panel.links) {
+          _.each(ctrl.panel.links, function(link) {
+            var info = linkSrv.getPanelLinkAnchorInfo(link, ctrl.panel.scopedVars);
             template += '<a class="panel-menu-link" href="' + info.href + '" target="' + info.target + '">' + info.title + '</a>';
             template += '<a class="panel-menu-link" href="' + info.href + '" target="' + info.target + '">' + info.title + '</a>';
           });
           });
         }
         }
         return template;
         return template;
       }
       }
-      function createMenuTemplate($scope) {
+
+      function createMenuTemplate(ctrl) {
         var template = '<div class="panel-menu small">';
         var template = '<div class="panel-menu small">';
 
 
-        if ($scope.dashboardMeta.canEdit) {
+        if (ctrl.dashboard.meta.canEdit) {
           template += '<div class="panel-menu-inner">';
           template += '<div class="panel-menu-inner">';
           template += '<div class="panel-menu-row">';
           template += '<div class="panel-menu-row">';
-          template += '<a class="panel-menu-icon pull-left" ng-click="updateColumnSpan(-1)"><i class="fa fa-minus"></i></a>';
-          template += '<a class="panel-menu-icon pull-left" ng-click="updateColumnSpan(1)"><i class="fa fa-plus"></i></a>';
-          template += '<a class="panel-menu-icon pull-right" ng-click="removePanel(panel)"><i class="fa fa-remove"></i></a>';
+          template += '<a class="panel-menu-icon pull-left" ng-click="ctrl.updateColumnSpan(-1)"><i class="fa fa-minus"></i></a>';
+          template += '<a class="panel-menu-icon pull-left" ng-click="ctrl.updateColumnSpan(1)"><i class="fa fa-plus"></i></a>';
+          template += '<a class="panel-menu-icon pull-right" ng-click="ctrl.removePanel()"><i class="fa fa-remove"></i></a>';
           template += '<div class="clearfix"></div>';
           template += '<div class="clearfix"></div>';
           template += '</div>';
           template += '</div>';
         }
         }
@@ -44,9 +45,9 @@ function (angular, $, _) {
         template += '<div class="panel-menu-row">';
         template += '<div class="panel-menu-row">';
         template += '<a class="panel-menu-link" gf-dropdown="extendedMenu"><i class="fa fa-bars"></i></a>';
         template += '<a class="panel-menu-link" gf-dropdown="extendedMenu"><i class="fa fa-bars"></i></a>';
 
 
-        _.each($scope.panelMeta.menu, function(item) {
+        _.each(ctrl.getMenu(), function(item) {
           // skip edit actions if not editor
           // skip edit actions if not editor
-          if (item.role === 'Editor' && !$scope.dashboardMeta.canEdit) {
+          if (item.role === 'Editor' && !ctrl.dashboard.meta.canEdit) {
             return;
             return;
           }
           }
 
 
@@ -63,8 +64,8 @@ function (angular, $, _) {
         return template;
         return template;
       }
       }
 
 
-      function getExtendedMenu($scope) {
-        return angular.copy($scope.panelMeta.extendedMenu);
+      function getExtendedMenu(ctrl) {
+        return ctrl.getExtendedMenu();
       }
       }
 
 
       return {
       return {
@@ -74,13 +75,14 @@ function (angular, $, _) {
           var $panelLinksBtn = $link.find(".panel-links-btn");
           var $panelLinksBtn = $link.find(".panel-links-btn");
           var $panelContainer = elem.parents(".panel-container");
           var $panelContainer = elem.parents(".panel-container");
           var menuScope = null;
           var menuScope = null;
+          var ctrl = $scope.ctrl;
           var timeout = null;
           var timeout = null;
           var $menu = null;
           var $menu = null;
 
 
           elem.append($link);
           elem.append($link);
 
 
-          $scope.$watchCollection('panel.links', function(newValue) {
-            var showIcon = (newValue ? newValue.length > 0 : false) && $scope.panel.title !== '';
+          $scope.$watchCollection('ctrl.panel.links', function(newValue) {
+            var showIcon = (newValue ? newValue.length > 0 : false) && ctrl.panel.title !== '';
             $panelLinksBtn.toggle(showIcon);
             $panelLinksBtn.toggle(showIcon);
           });
           });
 
 
@@ -95,7 +97,7 @@ function (angular, $, _) {
 
 
             // if hovering or draging pospone close
             // if hovering or draging pospone close
             if (force !== true) {
             if (force !== true) {
-              if ($menu.is(':hover') || $scope.dashboard.$$panelDragging) {
+              if ($menu.is(':hover') || $scope.ctrl.dashboard.$$panelDragging) {
                 dismiss(2200);
                 dismiss(2200);
                 return;
                 return;
               }
               }
@@ -124,9 +126,9 @@ function (angular, $, _) {
 
 
             var menuTemplate;
             var menuTemplate;
             if ($(e.target).hasClass('fa-external-link')) {
             if ($(e.target).hasClass('fa-external-link')) {
-              menuTemplate = createExternalLinkMenu($scope);
+              menuTemplate = createExternalLinkMenu(ctrl);
             } else {
             } else {
-              menuTemplate = createMenuTemplate($scope);
+              menuTemplate = createMenuTemplate(ctrl);
             }
             }
 
 
             $menu = $(menuTemplate);
             $menu = $(menuTemplate);
@@ -135,7 +137,7 @@ function (angular, $, _) {
             });
             });
 
 
             menuScope = $scope.$new();
             menuScope = $scope.$new();
-            menuScope.extendedMenu = getExtendedMenu($scope);
+            menuScope.extendedMenu = getExtendedMenu(ctrl);
             menuScope.dismiss = function() {
             menuScope.dismiss = function() {
               dismiss(null, true);
               dismiss(null, true);
             };
             };

+ 0 - 7
public/app/features/panel/panel_meta.js

@@ -1,7 +0,0 @@
-define([
-  './panel_meta2',
-],
-function (panelMeta) {
-  'use strict';
-  return panelMeta.default;
-});

+ 0 - 48
public/app/features/panel/panel_meta2.ts

@@ -1,48 +0,0 @@
-export default class PanelMeta {
-  description: any;
-  fullscreen: any;
-  editIcon: any;
-  panelName: any;
-  menu: any;
-  editorTabs: any;
-  extendedMenu: any;
-
-  constructor(options: any) {
-    this.description = options.description;
-    this.fullscreen = options.fullscreen;
-    this.editIcon = options.editIcon;
-    this.panelName = options.panelName;
-    this.menu = [];
-    this.editorTabs = [];
-    this.extendedMenu = [];
-
-    if (options.fullscreen) {
-      this.addMenuItem('View', 'icon-eye-open', 'toggleFullscreen(false); dismiss();');
-    }
-
-    this.addMenuItem('Edit', 'icon-cog', 'editPanel(); dismiss();', 'Editor');
-    this.addMenuItem('Duplicate', 'icon-copy', 'duplicatePanel()', 'Editor');
-    this.addMenuItem('Share', 'icon-share', 'sharePanel(); dismiss();');
-
-    this.addEditorTab('General', 'app/partials/panelgeneral.html');
-
-    if (options.metricsEditor) {
-      this.addEditorTab('Metrics', 'app/partials/metrics.html');
-    }
-
-    this.addExtendedMenuItem('Panel JSON', '', 'editPanelJson(); dismiss();');
-  }
-
-  addMenuItem (text, icon, click, role?) {
-    this.menu.push({text: text, icon: icon, click: click, role: role});
-  }
-
-  addExtendedMenuItem (text, icon, click, role?) {
-    this.extendedMenu.push({text: text, icon: icon, click: click, role: role});
-  }
-
-  addEditorTab (title, src) {
-    this.editorTabs.push({title: title, src: src});
-  }
-}
-

+ 0 - 162
public/app/features/panel/panel_srv.js

@@ -1,162 +0,0 @@
-define([
-  'angular',
-  'lodash',
-  'app/core/config',
-],
-function (angular, _, config) {
-  'use strict';
-
-  var module = angular.module('grafana.services');
-
-  module.service('panelSrv', function($rootScope, $timeout, datasourceSrv, $q) {
-
-    this.init = function($scope) {
-
-      if (!$scope.panel.span) { $scope.panel.span = 12; }
-
-      $scope.inspector = {};
-
-      $scope.editPanel = function() {
-        $scope.toggleFullscreen(true);
-      };
-
-      $scope.sharePanel = function() {
-        $scope.appEvent('show-modal', {
-          src: './app/features/dashboard/partials/shareModal.html',
-          scope: $scope.$new()
-        });
-      };
-
-      $scope.editPanelJson = function() {
-        $scope.appEvent('show-json-editor', { object: $scope.panel, updateHandler: $scope.replacePanel });
-      };
-
-      $scope.duplicatePanel = function() {
-        $scope.dashboard.duplicatePanel($scope.panel, $scope.row);
-      };
-
-      $scope.updateColumnSpan = function(span) {
-        $scope.updatePanelSpan($scope.panel, span);
-
-        $timeout(function() {
-          $scope.$broadcast('render');
-        });
-      };
-
-      $scope.addDataQuery = function(datasource) {
-        $scope.dashboard.addDataQueryTo($scope.panel, datasource);
-      };
-
-      $scope.removeDataQuery = function (query) {
-        $scope.dashboard.removeDataQuery($scope.panel, query);
-        $scope.get_data();
-      };
-
-      $scope.duplicateDataQuery = function(query) {
-        $scope.dashboard.duplicateDataQuery($scope.panel, query);
-      };
-
-      $scope.moveDataQuery = function(fromIndex, toIndex) {
-        $scope.dashboard.moveDataQuery($scope.panel, fromIndex, toIndex);
-      };
-
-      $scope.setDatasource = function(datasource) {
-        // switching to mixed
-        if (datasource.meta.mixed) {
-          _.each($scope.panel.targets, function(target) {
-            target.datasource = $scope.panel.datasource;
-            if (target.datasource === null) {
-              target.datasource = config.defaultDatasource;
-            }
-          });
-        }
-        // switching from mixed
-        else if ($scope.datasource && $scope.datasource.meta.mixed) {
-          _.each($scope.panel.targets, function(target) {
-            delete target.datasource;
-          });
-        }
-
-        $scope.panel.datasource = datasource.value;
-        $scope.datasource = null;
-        $scope.get_data();
-      };
-
-      $scope.toggleEditorHelp = function(index) {
-        if ($scope.editorHelpIndex === index) {
-          $scope.editorHelpIndex = null;
-          return;
-        }
-        $scope.editorHelpIndex = index;
-      };
-
-      $scope.isNewPanel = function() {
-        return $scope.panel.title === config.new_panel_title;
-      };
-
-      $scope.toggleFullscreen = function(edit) {
-        $scope.dashboardViewState.update({ fullscreen: true, edit: edit, panelId: $scope.panel.id });
-      };
-
-      $scope.otherPanelInFullscreenMode = function() {
-        return $scope.dashboardViewState.fullscreen && !$scope.fullscreen;
-      };
-
-      $scope.getCurrentDatasource = function() {
-        if ($scope.datasource) {
-          return $q.when($scope.datasource);
-        }
-
-        return datasourceSrv.get($scope.panel.datasource);
-      };
-
-      $scope.panelRenderingComplete = function() {
-        $rootScope.performance.panelsRendered++;
-      };
-
-      $scope.get_data = function() {
-        if ($scope.otherPanelInFullscreenMode()) { return; }
-
-        if ($scope.panel.snapshotData) {
-          if ($scope.loadSnapshot) {
-            $scope.loadSnapshot($scope.panel.snapshotData);
-          }
-          return;
-        }
-
-        delete $scope.panelMeta.error;
-        $scope.panelMeta.loading = true;
-
-        $scope.getCurrentDatasource().then(function(datasource) {
-          $scope.datasource = datasource;
-          return $scope.refreshData($scope.datasource) || $q.when({});
-        }).then(function() {
-          $scope.panelMeta.loading = false;
-        }, function(err) {
-          console.log('Panel data error:', err);
-          $scope.panelMeta.loading = false;
-          $scope.panelMeta.error = err.message || "Timeseries data request error";
-          $scope.inspector.error = err;
-        });
-      };
-
-      if ($scope.refreshData) {
-        $scope.$on("refresh", $scope.get_data);
-      }
-
-      // Post init phase
-      $scope.fullscreen = false;
-      $scope.editor = { index: 1 };
-
-      $scope.dashboardViewState.registerPanel($scope);
-      $scope.datasources = datasourceSrv.getMetricSources();
-
-      if (!$scope.skipDataOnInit) {
-        $timeout(function() {
-          $scope.get_data();
-        }, 30);
-      }
-    };
-  });
-
-});

+ 12 - 12
public/app/features/panel/partials/panel.html

@@ -1,12 +1,12 @@
-<div class="panel-container" ng-class="{'panel-transparent': panel.transparent}">
+<div class="panel-container" ng-class="{'panel-transparent': ctrl.panel.transparent}">
 	<div class="panel-header">
 	<div class="panel-header">
-		<span class="alert-error panel-error small pointer" config-modal="app/partials/inspector.html" ng-if="panelMeta.error">
-			<span data-placement="top" bs-tooltip="panelMeta.error">
+		<span class="alert-error panel-error small pointer" config-modal="app/partials/inspector.html" ng-if="ctrl.error">
+			<span data-placement="top" bs-tooltip="ctrl.error">
 				<i class="fa fa-exclamation"></i><span class="panel-error-arrow"></span>
 				<i class="fa fa-exclamation"></i><span class="panel-error-arrow"></span>
 			</span>
 			</span>
 		</span>
 		</span>
 
 
-		<span class="panel-loading" ng-show="panelMeta.loading">
+		<span class="panel-loading" ng-show="ctrl.loading">
 			<i class="fa fa-spinner fa-spin"></i>
 			<i class="fa fa-spinner fa-spin"></i>
 		</span>
 		</span>
 
 
@@ -19,27 +19,27 @@
 	<panel-resizer></panel-resizer>
 	<panel-resizer></panel-resizer>
 </div>
 </div>
 
 
-<div class="panel-full-edit" ng-if="editMode">
+<div class="panel-full-edit" ng-if="ctrl.editMode">
 	<div class="gf-box">
 	<div class="gf-box">
 		<div class="gf-box-header">
 		<div class="gf-box-header">
 			<div class="gf-box-title">
 			<div class="gf-box-title">
-				<i ng-class="panelMeta.editIcon"></i>
-				{{panelMeta.panelName}}
+				<i ng-class="ctrl.icon"></i>
+				{{ctrl.name}}
 			</div>
 			</div>
 
 
-			<div ng-model="editor.index" bs-tabs>
-				<div ng-repeat="tab in panelMeta.editorTabs" data-title="{{tab.title}}">
+			<div ng-model="ctrl.editorTabIndex" bs-tabs>
+				<div ng-repeat="tab in ctrl.editorTabs" data-title="{{tab.title}}">
 				</div>
 				</div>
 			</div>
 			</div>
 
 
-			<button class="gf-box-header-close-btn" ng-click="exitFullscreen();">
+			<button class="gf-box-header-close-btn" ng-click="ctrl.exitFullscreen();">
 				Back to dashboard
 				Back to dashboard
 			</button>
 			</button>
 		</div>
 		</div>
 
 
 		<div class="gf-box-body">
 		<div class="gf-box-body">
-			<div ng-repeat="tab in panelMeta.editorTabs" ng-if="editor.index === $index">
-				<div ng-include src="tab.src"></div>
+			<div ng-repeat="tab in ctrl.editorTabs" ng-if="ctrl.editorTabIndex === $index">
+				<panel-editor-tab editor-tab="tab" ctrl="ctrl" index="$index"></panel-editor-tab>
 			</div>
 			</div>
 		</div>
 		</div>
 	</div>
 	</div>

+ 7 - 7
public/app/features/panel/partials/panelTime.html

@@ -13,8 +13,8 @@
 				</li>
 				</li>
 				<li>
 				<li>
 					<input type="text" class="input-small tight-form-input last" placeholder="1h"
 					<input type="text" class="input-small tight-form-input last" placeholder="1h"
-					  empty-to-null ng-model="panel.timeFrom" valid-time-span
-					  ng-change="get_data()" ng-model-onblur>
+					  empty-to-null ng-model="ctrl.panel.timeFrom" valid-time-span
+					  ng-change="ctrl.refresh()" ng-model-onblur>
 				</li>
 				</li>
 			</ul>
 			</ul>
 			<div class="clearfix"></div>
 			<div class="clearfix"></div>
@@ -32,8 +32,8 @@
 				</li>
 				</li>
 				<li>
 				<li>
 					<input type="text" class="input-small tight-form-input last" placeholder="1h"
 					<input type="text" class="input-small tight-form-input last" placeholder="1h"
-					empty-to-null ng-model="panel.timeShift" valid-time-span
-					ng-change="get_data()" ng-model-onblur>
+					empty-to-null ng-model="ctrl.panel.timeShift" valid-time-span
+					ng-change="ctrl.refresh()" ng-model-onblur>
 				</li>
 				</li>
 			</ul>
 			</ul>
 			<div class="clearfix"></div>
 			<div class="clearfix"></div>
@@ -47,9 +47,9 @@
 					<strong>Hide time override info</strong>
 					<strong>Hide time override info</strong>
 				</li>
 				</li>
 				<li class="tight-form-item last">
 				<li class="tight-form-item last">
-					<input class="cr1" id="panel.hideTimeOverride" type="checkbox"
-					ng-model="panel.hideTimeOverride" ng-checked="panel.hideTimeOverride" ng-change="get_data()">
-					<label for="panel.hideTimeOverride" class="cr1"></label>
+					<input class="cr1" id="ctrl.panel.hideTimeOverride" type="checkbox"
+					ng-model="ctrl.panel.hideTimeOverride" ng-checked="ctrl.panel.hideTimeOverride" ng-change="ctrl.refresh()">
+					<label for="ctrl.panel.hideTimeOverride" class="cr1"></label>
 				</li>
 				</li>
 			</ul>
 			</ul>
 			<div class="clearfix"></div>
 			<div class="clearfix"></div>

+ 2 - 1
public/app/features/panel/partials/soloPanel.html

@@ -2,7 +2,8 @@
 	<div class="row-fluid">
 	<div class="row-fluid">
 		<div class="span12">
 		<div class="span12">
 			<div class="panel nospace" ng-if="panel" style="width: 100%">
 			<div class="panel nospace" ng-if="panel" style="width: 100%">
-				<panel-loader type="panel.type" ng-cloak></panel-loader>
+				<panel-loader dashboard="dashboard" row="row" panel="panel">
+				</panel-loader>
 			</div>
 			</div>
 		</div>
 		</div>
 </div>
 </div>

+ 4 - 4
public/app/features/panel/query_editor.ts

@@ -5,9 +5,9 @@ import angular from 'angular';
 /** @ngInject */
 /** @ngInject */
 function metricsQueryEditor(dynamicDirectiveSrv, datasourceSrv) {
 function metricsQueryEditor(dynamicDirectiveSrv, datasourceSrv) {
   return dynamicDirectiveSrv.create({
   return dynamicDirectiveSrv.create({
-    watchPath: "panel.datasource",
+    watchPath: "ctrl.panel.datasource",
     directive: scope => {
     directive: scope => {
-      let datasource = scope.target.datasource || scope.panel.datasource;
+      let datasource = scope.target.datasource || scope.ctrl.panel.datasource;
       return datasourceSrv.get(datasource).then(ds => {
       return datasourceSrv.get(datasource).then(ds => {
         scope.datasource = ds;
         scope.datasource = ds;
 
 
@@ -29,9 +29,9 @@ function metricsQueryEditor(dynamicDirectiveSrv, datasourceSrv) {
 /** @ngInject */
 /** @ngInject */
 function metricsQueryOptions(dynamicDirectiveSrv, datasourceSrv) {
 function metricsQueryOptions(dynamicDirectiveSrv, datasourceSrv) {
   return dynamicDirectiveSrv.create({
   return dynamicDirectiveSrv.create({
-    watchPath: "panel.datasource",
+    watchPath: "ctrl.panel.datasource",
     directive: scope => {
     directive: scope => {
-      return datasourceSrv.get(scope.panel.datasource).then(ds => {
+      return datasourceSrv.get(scope.ctrl.panel.datasource).then(ds => {
         return System.import(ds.meta.module).then(dsModule => {
         return System.import(ds.meta.module).then(dsModule => {
           return {
           return {
             name: 'metrics-query-options-' + ds.meta.id,
             name: 'metrics-query-options-' + ds.meta.id,

+ 1 - 6
public/app/features/panel/solo_panel_ctrl.js

@@ -39,13 +39,8 @@ function (angular, $) {
       }
       }
 
 
       $scope.panel.span = 12;
       $scope.panel.span = 12;
-      $scope.dashboardViewState = {registerPanel: function() { }, state: {}};
     };
     };
 
 
-    if (!$scope.skipAutoInit) {
-      $scope.init();
-    }
-
+    $scope.init();
   });
   });
-
 });
 });

+ 1 - 1
public/app/features/panellinks/module.js

@@ -15,7 +15,7 @@ function (angular, _) {
         },
         },
         restrict: 'E',
         restrict: 'E',
         controller: 'PanelLinksEditorCtrl',
         controller: 'PanelLinksEditorCtrl',
-        templateUrl: 'app/features/panellinks/module.html',
+        templateUrl: 'public/app/features/panellinks/module.html',
         link: function() {
         link: function() {
         }
         }
       };
       };

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

@@ -1,5 +1,6 @@
 define([
 define([
   './playlists_ctrl',
   './playlists_ctrl',
+  './playlist_search',
   './playlist_srv',
   './playlist_srv',
   './playlist_edit_ctrl',
   './playlist_edit_ctrl',
   './playlist_routes'
   './playlist_routes'

+ 47 - 41
public/app/features/playlist/partials/playlist.html

@@ -1,14 +1,14 @@
 <navbar title="Playlists" title-url="playlists" icon="fa fa-fw fa-list" subnav="true">
 <navbar title="Playlists" title-url="playlists" icon="fa fa-fw fa-list" subnav="true">
 	<ul class="nav">
 	<ul class="nav">
-		<li ng-class="{active: isNew()}" ng-show="isNew()"><a href="datasources/create">New</a></li>
-		<li class="active" ng-show="!isNew()"><a href="playlists/edit/{{playlist.id}}">{{playlist.name}}</a></li>
+		<li ng-class="{active: ctrl.isNew()}" ng-show="ctrl.isNew()"><a href="datasources/create">New</a></li>
+		<li class="active" ng-show="!ctrl.isNew()"><a href="playlists/edit/{{ctrl.playlist.id}}">{{ctrl.playlist.name}}</a></li>
 	</ul>
 	</ul>
 </navbar>
 </navbar>
 
 
 <div class="page-container" ng-form="playlistEditForm">
 <div class="page-container" ng-form="playlistEditForm">
   <div class="page">
   <div class="page">
-    <h2 ng-show="isNew()">New playlist</h2>
-    <h2 ng-show="!isNew()">Edit playlist</h2>
+    <h2 ng-show="ctrl.isNew()">New playlist</h2>
+    <h2 ng-show="!ctrl.isNew()">Edit playlist</h2>
 
 
     <h4>Name and interval</h4>
     <h4>Name and interval</h4>
 
 
@@ -20,7 +20,7 @@
               Name
               Name
             </li>
             </li>
             <li>
             <li>
-              <input type="text" required ng-model="playlist.name" class="input-xlarge tight-form-input">
+              <input type="text" required ng-model="ctrl.playlist.name" class="input-xlarge tight-form-input">
             </li>
             </li>
           </ul>
           </ul>
           <div class="clearfix"></div>
           <div class="clearfix"></div>
@@ -31,7 +31,7 @@
               Interval
               Interval
             </li>
             </li>
             <li>
             <li>
-              <input type="text" required ng-model="playlist.interval" placeholder="5m" class="input-xlarge tight-form-input">
+              <input type="text" required ng-model="ctrl.playlist.interval" placeholder="5m" class="input-xlarge tight-form-input">
             </li>
             </li>
           </ul>
           </ul>
           <div class="clearfix"></div>
           <div class="clearfix"></div>
@@ -39,66 +39,72 @@
       </div>
       </div>
 
 
       <br>
       <br>
-      <h4>Add dashboards</h4>
 
 
-      <div style="display: inline-block">
-        <div class="tight-form last">
-          <ul class="tight-form-list">
-						<li class="tight-form-item">
-							Search
-						</li>
-            <li>
-              <input type="text"
-                     class="tight-form-input input-xlarge last"
-                     ng-model="searchQuery"
-                     placeholder="dashboard search term"
-                     ng-trim="true"
-                     ng-change="search()">
-            </li>
-          </ul>
-          <div class="clearfix"></div>
-        </div>
-      </div>
     </div>
     </div>
   </div>
   </div>
 
 
   <div class="row">
   <div class="row">
     <div class="span5 pull-left">
     <div class="span5 pull-left">
-			<h5>Search results ({{filteredPlaylistItems.length}})</h5>
+      <h5>Add dashboards</h5>
+      <div style="">
+        <playlist-search class="playlist-search-container" search-started="ctrl.searchStarted(promise)"></playlist-search>
+      </div>
+    </div>
+  </div>
+
+  <div class="row">
+    <div class="span5 pull-left" ng-if="ctrl.filteredDashboards.length > 0">
+			<h5>Search results ({{ctrl.filteredDashboards.length}})</h5>
        <table class="grafana-options-table">
        <table class="grafana-options-table">
-        <tr ng-repeat="playlistItem in filteredPlaylistItems">
+        <tr ng-repeat="playlistItem in ctrl.filteredDashboards">
           <td style="white-space: nowrap;">
           <td style="white-space: nowrap;">
             {{playlistItem.title}}
             {{playlistItem.title}}
           </td>
           </td>
           <td style="text-align: center">
           <td style="text-align: center">
-            <button class="btn btn-inverse btn-mini pull-right" ng-click="addPlaylistItem(playlistItem)">
+            <button class="btn btn-inverse btn-mini pull-right" ng-click="ctrl.addPlaylistItem(playlistItem)">
               <i class="fa fa-plus"></i>
               <i class="fa fa-plus"></i>
               Add to playlist
               Add to playlist
             </button>
             </button>
           </td>
           </td>
         </tr>
         </tr>
-        <tr ng-if="isSearchResultsEmpty()">
-          <td colspan="2">
-            <i class="fa fa-warning"></i> Search results empty
-          </td>
-        </tr>
       </table>
       </table>
     </div>
     </div>
+    <div class="playlist-search-results-container" ng-if="ctrl.filteredTags.length > 0">
+      <div class="row">
+        <div class="span6 offset1">
+          <div ng-repeat="tag in ctrl.filteredTags" class="pointer" style="width: 180px; float: left;"
+            ng-class="{'selected': $index === selectedIndex }"
+            ng-click="ctrl.addTagPlaylistItem(tag, $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>
     <div class="span5 pull-left">
     <div class="span5 pull-left">
       <h5>Added dashboards</h5>
       <h5>Added dashboards</h5>
       <table class="grafana-options-table">
       <table class="grafana-options-table">
-        <tr ng-repeat="playlistItem in playlistItems">
-          <td style="white-space: nowrap;">
+        <tr ng-repeat="playlistItem in ctrl.playlistItems">
+          <td style="white-space: nowrap;" ng-if="playlistItem.type === 'dashboard_by_id'">
             {{playlistItem.title}}
             {{playlistItem.title}}
           </td>
           </td>
+          <td style="white-space: nowrap;"  ng-if="playlistItem.type === 'dashboard_by_tag'">
+            <a class="search-result-tag label label-tag" tag-color-from-name="playlistItem.title">
+              <i class="fa fa-tag"></i>
+              <span>{{playlistItem.title}}</span>
+            </a>
+          </td>
+
           <td style="text-align: right">
           <td style="text-align: right">
-            <button class="btn btn-inverse btn-mini" ng-hide="$first" ng-click="movePlaylistItemUp(playlistItem)">
+            <button class="btn btn-inverse btn-mini" ng-hide="$first" ng-click="ctrl.movePlaylistItemUp(playlistItem)">
               <i class="fa fa-arrow-up"></i>
               <i class="fa fa-arrow-up"></i>
             </button>
             </button>
-            <button class="btn btn-inverse btn-mini" ng-hide="$last" ng-click="movePlaylistItemDown(playlistItem)">
+            <button class="btn btn-inverse btn-mini" ng-hide="$last" ng-click="ctrl.movePlaylistItemDown(playlistItem)">
               <i class="fa fa-arrow-down"></i>
               <i class="fa fa-arrow-down"></i>
             </button>
             </button>
-            <button class="btn btn-inverse btn-mini" ng-click="removePlaylistItem(playlistItem)">
+            <button class="btn btn-inverse btn-mini" ng-click="ctrl.removePlaylistItem(playlistItem)">
               <i class="fa fa-remove"></i>
               <i class="fa fa-remove"></i>
             </button>
             </button>
           </td>
           </td>
@@ -113,11 +119,11 @@
     <!-- <div class="tight-form"> -->
     <!-- <div class="tight-form"> -->
       <button type="button"
       <button type="button"
               class="btn btn-success"
               class="btn btn-success"
-              ng-disabled="playlistEditForm.$invalid || isPlaylistEmpty()"
-              ng-click="savePlaylist(playlist, playlistItems)">Save</button>
+              ng-disabled="ctrl.playlistEditForm.$invalid || ctrl.isPlaylistEmpty()"
+              ng-click="ctrl.savePlaylist(ctrl.playlist, ctrl.playlistItems)">Save</button>
       <button type="button"
       <button type="button"
               class="btn btn-inverse"
               class="btn btn-inverse"
-              ng-click="backToList()">Cancel</button>
+              ng-click="ctrl.backToList()">Cancel</button>
     <!-- </div> -->
     <!-- </div> -->
   </div>
   </div>
 
 

+ 26 - 0
public/app/features/playlist/partials/playlist_search.html

@@ -0,0 +1,26 @@
+<div class="playlist-search-field-wrapper">
+  <span style="position: relative;">
+    <input  type="text" placeholder="Find dashboards by name" tabindex="1"
+    ng-keydown="ctrl.keyDown($event)" ng-model="ctrl.query.query" ng-model-options="{ debounce: 500 }" spellcheck='false' ng-change="ctrl.searchDashboards()" />
+  </span>
+  <div class="playlist-search-switches">
+    <i class="fa fa-filter"></i>
+    <a class="pointer" href="javascript:void 0;" ng-click="ctrl.showStarred()" tabindex="2">
+      <i class="fa fa-remove" ng-show="ctrl.query.starred"></i>
+      starred
+    </a> |
+    <a class="pointer" href="javascript:void 0;" ng-click="ctrl.getTags()" tabindex="3">
+      <i class="fa fa-remove" ng-show="ctrl.tagsMode"></i>
+      tags
+    </a>
+    <span ng-if="ctrl.query.tag.length">
+      |
+      <span ng-repeat="tagName in ctrl.query.tag">
+        <a ng-click="ctrl.removeTag(tagName, $event)" tag-color-from-name="ctrl.tagName" class="label label-tag">
+          <i class="fa fa-remove"></i>
+          {{tagName}}
+        </a>
+      </span>
+    </span>
+  </div>
+</div>

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

@@ -19,7 +19,7 @@
         <th style="width: 25px"></th>
         <th style="width: 25px"></th>
 
 
       </thead>
       </thead>
-      <tr ng-repeat="playlist in playlists">
+      <tr ng-repeat="playlist in ctrl.playlists">
         <td>
         <td>
 					<a href="playlists/edit/{{playlist.id}}">{{playlist.name}}</a>
 					<a href="playlists/edit/{{playlist.id}}">{{playlist.name}}</a>
         </td>
         </td>
@@ -39,7 +39,7 @@
           </a>
           </a>
         </td>
         </td>
         <td  class="text-right">
         <td  class="text-right">
-          <a ng-click="removePlaylist(playlist)" class="btn btn-danger btn-mini">
+          <a ng-click="ctrl.removePlaylist(playlist)" class="btn btn-danger btn-mini">
             <i class="fa fa-remove"></i>
             <i class="fa fa-remove"></i>
           </a>
           </a>
         </td>
         </td>

Some files were not shown because too many files changed in this diff