浏览代码

Merge branch 'apps'

Torkel Ödegaard 10 年之前
父节点
当前提交
e5b3f27a30
共有 100 个文件被更改,包括 1577 次插入1027 次删除
  1. 9 2
      pkg/api/api.go
  2. 75 0
      pkg/api/api_plugin.go
  3. 59 0
      pkg/api/app_settings.go
  4. 13 6
      pkg/api/datasources.go
  5. 33 0
      pkg/api/dtos/apps.go
  6. 5 4
      pkg/api/dtos/index.go
  7. 0 8
      pkg/api/dtos/plugin_bundle.go
  8. 8 3
      pkg/api/frontendsettings.go
  9. 32 2
      pkg/api/index.go
  10. 3 3
      pkg/cmd/web.go
  11. 4 0
      pkg/middleware/middleware.go
  12. 9 8
      pkg/models/app_settings.go
  13. 11 0
      pkg/models/org_user.go
  14. 43 0
      pkg/plugins/app_plugin.go
  15. 25 0
      pkg/plugins/datasource_plugin.go
  16. 48 0
      pkg/plugins/frontend_plugin.go
  17. 70 22
      pkg/plugins/models.go
  18. 19 0
      pkg/plugins/panel_plugin.go
  19. 78 46
      pkg/plugins/plugins.go
  20. 17 0
      pkg/plugins/plugins_test.go
  21. 75 0
      pkg/plugins/queries.go
  22. 50 0
      pkg/services/sqlstore/app_settings.go
  23. 9 7
      pkg/services/sqlstore/migrations/app_settings.go
  24. 1 1
      pkg/services/sqlstore/migrations/migrations.go
  25. 0 46
      pkg/services/sqlstore/plugin_bundle.go
  26. 0 6
      public/app/core/controllers/all.js
  27. 2 1
      public/app/core/controllers/sidemenu_ctrl.js
  28. 3 2
      public/app/core/directives/misc.js
  29. 15 12
      public/app/core/routes/all.js
  30. 4 0
      public/app/core/services/alert_srv.js
  31. 15 6
      public/app/core/services/datasource_srv.js
  32. 1 1
      public/app/features/annotations/partials/editor.html
  33. 2 0
      public/app/features/apps/all.ts
  34. 43 0
      public/app/features/apps/app_srv.ts
  35. 44 0
      public/app/features/apps/edit_ctrl.ts
  36. 19 0
      public/app/features/apps/list_ctrl.ts
  37. 102 0
      public/app/features/apps/partials/edit.html
  38. 51 0
      public/app/features/apps/partials/list.html
  39. 0 8
      public/app/features/dashboard/dashboardCtrl.js
  40. 4 0
      public/app/features/datasources/all.js
  41. 5 23
      public/app/features/datasources/edit_ctrl.js
  42. 0 0
      public/app/features/datasources/list_ctrl.js
  43. 1 1
      public/app/features/datasources/partials/edit.html
  44. 2 0
      public/app/features/datasources/partials/http_settings.html
  45. 0 0
      public/app/features/datasources/partials/list.html
  46. 1 6
      public/app/features/org/all.js
  47. 0 3
      public/app/features/org/partials/pluginConfigCore.html
  48. 0 42
      public/app/features/org/partials/pluginEdit.html
  49. 0 41
      public/app/features/org/partials/plugins.html
  50. 0 35
      public/app/features/org/pluginEditCtrl.js
  51. 0 47
      public/app/features/org/plugin_directive.js
  52. 0 58
      public/app/features/org/plugin_srv.js
  53. 0 33
      public/app/features/org/pluginsCtrl.js
  54. 43 16
      public/app/features/panel/panel_directive.js
  55. 5 9
      public/app/partials/sidemenu.html
  56. 3 0
      public/app/plugins/datasource/cloudwatch/datasource.d.ts
  57. 24 29
      public/app/plugins/datasource/cloudwatch/datasource.js
  58. 9 1
      public/app/plugins/datasource/cloudwatch/module.js
  59. 0 0
      public/app/plugins/datasource/cloudwatch/partials/edit_view.html
  60. 2 11
      public/app/plugins/datasource/cloudwatch/plugin.json
  61. 10 10
      public/app/plugins/datasource/cloudwatch/specs/datasource_specs.ts
  62. 3 0
      public/app/plugins/datasource/elasticsearch/datasource.d.ts
  63. 36 42
      public/app/plugins/datasource/elasticsearch/datasource.js
  64. 4 0
      public/app/plugins/datasource/elasticsearch/directives.js
  65. 38 0
      public/app/plugins/datasource/elasticsearch/edit_view.ts
  66. 60 0
      public/app/plugins/datasource/elasticsearch/module.js
  67. 3 4
      public/app/plugins/datasource/elasticsearch/partials/edit_view.html
  68. 2 11
      public/app/plugins/datasource/elasticsearch/plugin.json
  69. 16 16
      public/app/plugins/datasource/elasticsearch/specs/datasource_specs.ts
  70. 0 30
      public/app/plugins/datasource/grafana/datasource.js
  71. 0 13
      public/app/plugins/datasource/grafana/directives.js
  72. 3 6
      public/app/plugins/datasource/grafana/plugin.json
  73. 3 0
      public/app/plugins/datasource/graphite/datasource.d.ts
  74. 55 63
      public/app/plugins/datasource/graphite/datasource.js
  75. 9 1
      public/app/plugins/datasource/graphite/module.js
  76. 1 2
      public/app/plugins/datasource/graphite/partials/config.html
  77. 2 10
      public/app/plugins/datasource/graphite/plugin.json
  78. 9 4
      public/app/plugins/datasource/graphite/specs/datasource_specs.ts
  79. 20 27
      public/app/plugins/datasource/influxdb/datasource.js
  80. 9 1
      public/app/plugins/datasource/influxdb/module.js
  81. 1 2
      public/app/plugins/datasource/influxdb/partials/config.html
  82. 2 10
      public/app/plugins/datasource/influxdb/plugin.json
  83. 0 35
      public/app/plugins/datasource/mixed/datasource.js
  84. 32 0
      public/app/plugins/datasource/mixed/datasource.ts
  85. 3 6
      public/app/plugins/datasource/mixed/plugin.json
  86. 3 0
      public/app/plugins/datasource/opentsdb/datasource.d.ts
  87. 16 22
      public/app/plugins/datasource/opentsdb/datasource.js
  88. 9 1
      public/app/plugins/datasource/opentsdb/module.js
  89. 1 3
      public/app/plugins/datasource/opentsdb/partials/config.html
  90. 2 10
      public/app/plugins/datasource/opentsdb/plugin.json
  91. 71 0
      public/app/plugins/datasource/opentsdb/specs/datasource-specs.ts
  92. 3 0
      public/app/plugins/datasource/prometheus/datasource.d.ts
  93. 34 41
      public/app/plugins/datasource/prometheus/datasource.js
  94. 9 1
      public/app/plugins/datasource/prometheus/module.js
  95. 1 3
      public/app/plugins/datasource/prometheus/partials/config.html
  96. 2 10
      public/app/plugins/datasource/prometheus/plugin.json
  97. 9 6
      public/app/plugins/datasource/prometheus/specs/datasource_specs.ts
  98. 0 18
      public/app/plugins/datasource/sql/datasource.js
  99. 0 53
      public/app/plugins/datasource/sql/partials/config.html
  100. 0 17
      public/app/plugins/datasource/sql/partials/query.editor.html

+ 9 - 2
pkg/api/api.go

@@ -41,8 +41,8 @@ func Register(r *macaron.Macaron) {
 	r.Get("/admin/orgs", reqGrafanaAdmin, Index)
 	r.Get("/admin/orgs/edit/:id", reqGrafanaAdmin, Index)
 
-	r.Get("/plugins", reqSignedIn, Index)
-	r.Get("/plugins/edit/*", reqSignedIn, Index)
+	r.Get("/apps", reqSignedIn, Index)
+	r.Get("/apps/edit/*", reqSignedIn, Index)
 
 	r.Get("/dashboard/*", reqSignedIn, Index)
 	r.Get("/dashboard-solo/*", reqSignedIn, Index)
@@ -120,6 +120,11 @@ func Register(r *macaron.Macaron) {
 			r.Get("/invites", wrap(GetPendingOrgInvites))
 			r.Post("/invites", quota("user"), bind(dtos.AddInviteForm{}), wrap(AddOrgInvite))
 			r.Patch("/invites/:code/revoke", wrap(RevokeInvite))
+
+			// apps
+			r.Get("/apps", wrap(GetOrgAppsList))
+			r.Get("/apps/:appId/settings", wrap(GetAppSettingsById))
+			r.Post("/apps/:appId/settings", bind(m.UpdateAppSettingsCmd{}), wrap(UpdateAppSettings))
 		}, reqOrgAdmin)
 
 		// create new org
@@ -205,5 +210,7 @@ func Register(r *macaron.Macaron) {
 	// rendering
 	r.Get("/render/*", reqSignedIn, RenderToPng)
 
+	InitApiPluginRoutes(r)
+
 	r.NotFound(NotFoundHandler)
 }

+ 75 - 0
pkg/api/api_plugin.go

@@ -0,0 +1,75 @@
+package api
+
+import (
+	"encoding/json"
+	"net/http"
+	"net/http/httputil"
+	"net/url"
+
+	"github.com/Unknwon/macaron"
+	"github.com/grafana/grafana/pkg/log"
+	"github.com/grafana/grafana/pkg/middleware"
+	m "github.com/grafana/grafana/pkg/models"
+	"github.com/grafana/grafana/pkg/plugins"
+	"github.com/grafana/grafana/pkg/util"
+)
+
+func InitApiPluginRoutes(r *macaron.Macaron) {
+	for _, plugin := range plugins.ApiPlugins {
+		log.Info("Plugin: Adding proxy routes for api plugin")
+		for _, route := range plugin.Routes {
+			url := util.JoinUrlFragments("/api/plugin-proxy/", route.Path)
+			handlers := make([]macaron.Handler, 0)
+			if route.ReqSignedIn {
+				handlers = append(handlers, middleware.Auth(&middleware.AuthOptions{ReqSignedIn: true}))
+			}
+			if route.ReqGrafanaAdmin {
+				handlers = append(handlers, middleware.Auth(&middleware.AuthOptions{ReqSignedIn: true, ReqGrafanaAdmin: true}))
+			}
+			if route.ReqSignedIn && route.ReqRole != "" {
+				if route.ReqRole == m.ROLE_ADMIN {
+					handlers = append(handlers, middleware.RoleAuth(m.ROLE_ADMIN))
+				} else if route.ReqRole == m.ROLE_EDITOR {
+					handlers = append(handlers, middleware.RoleAuth(m.ROLE_EDITOR, m.ROLE_ADMIN))
+				}
+			}
+			handlers = append(handlers, ApiPlugin(route.Url))
+			r.Route(url, route.Method, handlers...)
+			log.Info("Plugin: Adding route %s", url)
+		}
+	}
+}
+
+func ApiPlugin(routeUrl string) macaron.Handler {
+	return func(c *middleware.Context) {
+		path := c.Params("*")
+
+		//Create a HTTP header with the context in it.
+		ctx, err := json.Marshal(c.SignedInUser)
+		if err != nil {
+			c.JsonApiErr(500, "failed to marshal context to json.", err)
+			return
+		}
+		targetUrl, _ := url.Parse(routeUrl)
+		proxy := NewApiPluginProxy(string(ctx), path, targetUrl)
+		proxy.Transport = dataProxyTransport
+		proxy.ServeHTTP(c.RW(), c.Req.Request)
+	}
+}
+
+func NewApiPluginProxy(ctx string, proxyPath string, targetUrl *url.URL) *httputil.ReverseProxy {
+	director := func(req *http.Request) {
+		req.URL.Scheme = targetUrl.Scheme
+		req.URL.Host = targetUrl.Host
+		req.Host = targetUrl.Host
+
+		req.URL.Path = util.JoinUrlFragments(targetUrl.Path, proxyPath)
+
+		// clear cookie headers
+		req.Header.Del("Cookie")
+		req.Header.Del("Set-Cookie")
+		req.Header.Add("Grafana-Context", ctx)
+	}
+
+	return &httputil.ReverseProxy{Director: director}
+}

+ 59 - 0
pkg/api/app_settings.go

@@ -0,0 +1,59 @@
+package api
+
+import (
+	"github.com/grafana/grafana/pkg/api/dtos"
+	"github.com/grafana/grafana/pkg/bus"
+	"github.com/grafana/grafana/pkg/middleware"
+	m "github.com/grafana/grafana/pkg/models"
+	"github.com/grafana/grafana/pkg/plugins"
+)
+
+func GetOrgAppsList(c *middleware.Context) Response {
+	orgApps, err := plugins.GetOrgAppSettings(c.OrgId)
+
+	if err != nil {
+		return ApiError(500, "Failed to list of apps", err)
+	}
+
+	result := make([]*dtos.AppSettings, 0)
+	for _, app := range plugins.Apps {
+		orgApp := orgApps[app.Id]
+		result = append(result, dtos.NewAppSettingsDto(app, orgApp))
+	}
+
+	return Json(200, result)
+}
+
+func GetAppSettingsById(c *middleware.Context) Response {
+	appId := c.Params(":appId")
+
+	if pluginDef, exists := plugins.Apps[appId]; !exists {
+		return ApiError(404, "PluginId not found, no installed plugin with that id", nil)
+	} else {
+		orgApps, err := plugins.GetOrgAppSettings(c.OrgId)
+		if err != nil {
+			return ApiError(500, "Failed to get org app settings ", nil)
+		}
+		orgApp := orgApps[appId]
+
+		return Json(200, dtos.NewAppSettingsDto(pluginDef, orgApp))
+	}
+}
+
+func UpdateAppSettings(c *middleware.Context, cmd m.UpdateAppSettingsCmd) Response {
+	appId := c.Params(":appId")
+
+	cmd.OrgId = c.OrgId
+	cmd.AppId = appId
+
+	if _, ok := plugins.Apps[cmd.AppId]; !ok {
+		return ApiError(404, "App type not installed.", nil)
+	}
+
+	err := bus.Dispatch(&cmd)
+	if err != nil {
+		return ApiError(500, "Failed to update App Plugin", err)
+	}
+
+	return ApiSuccess("App updated")
+}

+ 13 - 6
pkg/api/datasources.go

@@ -3,6 +3,7 @@ package api
 import (
 	"github.com/grafana/grafana/pkg/api/dtos"
 	"github.com/grafana/grafana/pkg/bus"
+	//"github.com/grafana/grafana/pkg/log"
 	"github.com/grafana/grafana/pkg/middleware"
 	m "github.com/grafana/grafana/pkg/models"
 	"github.com/grafana/grafana/pkg/plugins"
@@ -115,13 +116,19 @@ func UpdateDataSource(c *middleware.Context, cmd m.UpdateDataSourceCommand) {
 }
 
 func GetDataSourcePlugins(c *middleware.Context) {
-	dsList := make(map[string]interface{})
+	dsList := make(map[string]*plugins.DataSourcePlugin)
 
-	for key, value := range plugins.DataSources {
-		if !value.BuiltIn {
-			dsList[key] = value
+	if enabledPlugins, err := plugins.GetEnabledPlugins(c.OrgId); err != nil {
+		c.JsonApiErr(500, "Failed to get org apps", err)
+		return
+	} else {
+
+		for key, value := range enabledPlugins.DataSources {
+			if !value.BuiltIn {
+				dsList[key] = value
+			}
 		}
-	}
 
-	c.JSON(200, dsList)
+		c.JSON(200, dsList)
+	}
 }

+ 33 - 0
pkg/api/dtos/apps.go

@@ -0,0 +1,33 @@
+package dtos
+
+import (
+	"github.com/grafana/grafana/pkg/models"
+	"github.com/grafana/grafana/pkg/plugins"
+)
+
+type AppSettings struct {
+	Name     string                   `json:"name"`
+	AppId    string                   `json:"appId"`
+	Enabled  bool                     `json:"enabled"`
+	Pinned   bool                     `json:"pinned"`
+	Info     *plugins.PluginInfo      `json:"info"`
+	Pages    []*plugins.AppPluginPage `json:"pages"`
+	JsonData map[string]interface{}   `json:"jsonData"`
+}
+
+func NewAppSettingsDto(def *plugins.AppPlugin, data *models.AppSettings) *AppSettings {
+	dto := &AppSettings{
+		AppId: def.Id,
+		Name:  def.Name,
+		Info:  &def.Info,
+		Pages: def.Pages,
+	}
+
+	if data != nil {
+		dto.Enabled = data.Enabled
+		dto.Pinned = data.Pinned
+		dto.Info = &def.Info
+	}
+
+	return dto
+}

+ 5 - 4
pkg/api/dtos/index.go

@@ -8,9 +8,9 @@ type IndexViewData struct {
 	GoogleAnalyticsId  string
 	GoogleTagManagerId string
 
-	PluginCss    []*PluginCss
-	PluginJs     []string
-	MainNavLinks []*NavLink
+	PluginCss     []*PluginCss
+	PluginModules []string
+	MainNavLinks  []*NavLink
 }
 
 type PluginCss struct {
@@ -21,5 +21,6 @@ type PluginCss struct {
 type NavLink struct {
 	Text string `json:"text"`
 	Icon string `json:"icon"`
-	Href string `json:"href"`
+	Img  string `json:"img"`
+	Url  string `json:"url"`
 }

+ 0 - 8
pkg/api/dtos/plugin_bundle.go

@@ -1,8 +0,0 @@
-package dtos
-
-type PluginBundle struct {
-	Type     string                 `json:"type"`
-	Enabled  bool                   `json:"enabled"`
-	Module   string                 `json:"module"`
-	JsonData map[string]interface{} `json:"jsonData"`
-}

+ 8 - 3
pkg/api/frontendsettings.go

@@ -29,6 +29,11 @@ func getFrontendSettingsMap(c *middleware.Context) (map[string]interface{}, erro
 	datasources := make(map[string]interface{})
 	var defaultDatasource string
 
+	enabledPlugins, err := plugins.GetEnabledPlugins(c.OrgId)
+	if err != nil {
+		return nil, err
+	}
+
 	for _, ds := range orgDataSources {
 		url := ds.Url
 
@@ -42,7 +47,7 @@ func getFrontendSettingsMap(c *middleware.Context) (map[string]interface{}, erro
 			"url":  url,
 		}
 
-		meta, exists := plugins.DataSources[ds.Type]
+		meta, exists := enabledPlugins.DataSources[ds.Type]
 		if !exists {
 			log.Error(3, "Could not find plugin definition for data source: %v", ds.Type)
 			continue
@@ -110,8 +115,8 @@ func getFrontendSettingsMap(c *middleware.Context) (map[string]interface{}, erro
 	}
 
 	panels := map[string]interface{}{}
-	for _, panel := range plugins.Panels {
-		panels[panel.Type] = map[string]interface{}{
+	for _, panel := range enabledPlugins.Panels {
+		panels[panel.Id] = map[string]interface{}{
 			"module": panel.Module,
 			"name":   panel.Name,
 		}

+ 32 - 2
pkg/api/index.go

@@ -4,6 +4,7 @@ import (
 	"github.com/grafana/grafana/pkg/api/dtos"
 	"github.com/grafana/grafana/pkg/middleware"
 	m "github.com/grafana/grafana/pkg/models"
+	"github.com/grafana/grafana/pkg/plugins"
 	"github.com/grafana/grafana/pkg/setting"
 )
 
@@ -50,7 +51,7 @@ func setIndexViewData(c *middleware.Context) (*dtos.IndexViewData, error) {
 	data.MainNavLinks = append(data.MainNavLinks, &dtos.NavLink{
 		Text: "Dashboards",
 		Icon: "fa fa-fw fa-th-large",
-		Href: "/",
+		Url:  "/",
 	})
 
 	data.MainNavLinks = append(data.MainNavLinks, &dtos.NavLink{
@@ -63,8 +64,37 @@ func setIndexViewData(c *middleware.Context) (*dtos.IndexViewData, error) {
 		data.MainNavLinks = append(data.MainNavLinks, &dtos.NavLink{
 			Text: "Data Sources",
 			Icon: "fa fa-fw fa-database",
-			Href: "/datasources",
+			Url:  "/datasources",
 		})
+
+		data.MainNavLinks = append(data.MainNavLinks, &dtos.NavLink{
+			Text: "Apps",
+			Icon: "fa fa-fw fa-cubes",
+			Url:  "/apps",
+		})
+	}
+
+	enabledPlugins, err := plugins.GetEnabledPlugins(c.OrgId)
+	if err != nil {
+		return nil, err
+	}
+
+	for _, plugin := range enabledPlugins.Apps {
+		if plugin.Module != "" {
+			data.PluginModules = append(data.PluginModules, plugin.Module)
+		}
+
+		if plugin.Css != nil {
+			data.PluginCss = append(data.PluginCss, &dtos.PluginCss{Light: plugin.Css.Light, Dark: plugin.Css.Dark})
+		}
+
+		if plugin.Pinned {
+			data.MainNavLinks = append(data.MainNavLinks, &dtos.NavLink{
+				Text: plugin.Name,
+				Url:  "/apps/edit/" + plugin.Id,
+				Img:  plugin.Info.Logos.Small,
+			})
+		}
 	}
 
 	return &data, nil

+ 3 - 3
pkg/cmd/web.go

@@ -30,9 +30,9 @@ func newMacaron() *macaron.Macaron {
 	}
 
 	for _, route := range plugins.StaticRoutes {
-		pluginRoute := path.Join("/public/plugins/", route.Url)
-		log.Info("Plugin: Adding static route %s -> %s", pluginRoute, route.Path)
-		mapStatic(m, route.Path, "", pluginRoute)
+		pluginRoute := path.Join("/public/plugins/", route.PluginId)
+		log.Info("Plugin: Adding static route %s -> %s", pluginRoute, route.Directory)
+		mapStatic(m, route.Directory, "", pluginRoute)
 	}
 
 	mapStatic(m, setting.StaticRootPath, "", "public")

+ 4 - 0
pkg/middleware/middleware.go

@@ -253,3 +253,7 @@ func (ctx *Context) JsonApiErr(status int, message string, err error) {
 
 	ctx.JSON(status, resp)
 }
+
+func (ctx *Context) HasUserRole(role m.RoleType) bool {
+	return ctx.OrgRole.Includes(role)
+}

+ 9 - 8
pkg/models/plugin_bundle.go → pkg/models/app_settings.go

@@ -2,11 +2,12 @@ package models
 
 import "time"
 
-type PluginBundle struct {
+type AppSettings struct {
 	Id       int64
-	Type     string
+	AppId    string
 	OrgId    int64
 	Enabled  bool
+	Pinned   bool
 	JsonData map[string]interface{}
 
 	Created time.Time
@@ -17,18 +18,18 @@ type PluginBundle struct {
 // COMMANDS
 
 // Also acts as api DTO
-type UpdatePluginBundleCmd struct {
-	Type     string                 `json:"type" binding:"Required"`
+type UpdateAppSettingsCmd struct {
 	Enabled  bool                   `json:"enabled"`
+	Pinned   bool                   `json:"pinned"`
 	JsonData map[string]interface{} `json:"jsonData"`
 
-	Id    int64 `json:"-"`
-	OrgId int64 `json:"-"`
+	AppId string `json:"-"`
+	OrgId int64  `json:"-"`
 }
 
 // ---------------------
 // QUERIES
-type GetPluginBundlesQuery struct {
+type GetAppSettingsQuery struct {
 	OrgId  int64
-	Result []*PluginBundle
+	Result []*AppSettings
 }

+ 11 - 0
pkg/models/org_user.go

@@ -26,6 +26,17 @@ func (r RoleType) IsValid() bool {
 	return r == ROLE_VIEWER || r == ROLE_ADMIN || r == ROLE_EDITOR || r == ROLE_READ_ONLY_EDITOR
 }
 
+func (r RoleType) Includes(other RoleType) bool {
+	if r == ROLE_ADMIN {
+		return true
+	}
+	if r == ROLE_EDITOR || r == ROLE_READ_ONLY_EDITOR {
+		return other != ROLE_ADMIN
+	}
+
+	return r == other
+}
+
 type OrgUser struct {
 	Id      int64
 	OrgId   int64

+ 43 - 0
pkg/plugins/app_plugin.go

@@ -0,0 +1,43 @@
+package plugins
+
+import (
+	"encoding/json"
+
+	"github.com/grafana/grafana/pkg/models"
+)
+
+type AppPluginPage struct {
+	Name    string          `json:"name"`
+	Url     string          `json:"url"`
+	ReqRole models.RoleType `json:"reqRole"`
+}
+
+type AppPluginCss struct {
+	Light string `json:"light"`
+	Dark  string `json:"dark"`
+}
+
+type AppPlugin struct {
+	FrontendPluginBase
+	Css   *AppPluginCss    `json:"css"`
+	Pages []*AppPluginPage `json:"pages"`
+
+	Pinned  bool `json:"-"`
+	Enabled bool `json:"-"`
+}
+
+func (app *AppPlugin) Load(decoder *json.Decoder, pluginDir string) error {
+	if err := decoder.Decode(&app); err != nil {
+		return err
+	}
+
+	if app.Css != nil {
+		app.Css.Dark = evalRelativePluginUrlPath(app.Css.Dark, app.Id)
+		app.Css.Light = evalRelativePluginUrlPath(app.Css.Light, app.Id)
+	}
+
+	app.PluginDir = pluginDir
+	app.initFrontendPlugin()
+	Apps[app.Id] = app
+	return nil
+}

+ 25 - 0
pkg/plugins/datasource_plugin.go

@@ -0,0 +1,25 @@
+package plugins
+
+import "encoding/json"
+
+type DataSourcePlugin struct {
+	FrontendPluginBase
+	DefaultMatchFormat string `json:"defaultMatchFormat"`
+	Annotations        bool   `json:"annotations"`
+	Metrics            bool   `json:"metrics"`
+	BuiltIn            bool   `json:"builtIn"`
+	Mixed              bool   `json:"mixed"`
+	App                string `json:"app"`
+}
+
+func (p *DataSourcePlugin) Load(decoder *json.Decoder, pluginDir string) error {
+	if err := decoder.Decode(&p); err != nil {
+		return err
+	}
+
+	p.PluginDir = pluginDir
+	p.initFrontendPlugin()
+	DataSources[p.Id] = p
+
+	return nil
+}

+ 48 - 0
pkg/plugins/frontend_plugin.go

@@ -0,0 +1,48 @@
+package plugins
+
+import (
+	"net/url"
+	"path"
+	"path/filepath"
+)
+
+type FrontendPluginBase struct {
+	PluginBase
+	Module     string `json:"module"`
+	StaticRoot string `json:"staticRoot"`
+}
+
+func (fp *FrontendPluginBase) initFrontendPlugin() {
+	if fp.StaticRoot != "" {
+		StaticRoutes = append(StaticRoutes, &PluginStaticRoute{
+			Directory: filepath.Join(fp.PluginDir, fp.StaticRoot),
+			PluginId:  fp.Id,
+		})
+	}
+
+	fp.Info.Logos.Small = evalRelativePluginUrlPath(fp.Info.Logos.Small, fp.Id)
+	fp.Info.Logos.Large = evalRelativePluginUrlPath(fp.Info.Logos.Large, fp.Id)
+
+	fp.handleModuleDefaults()
+}
+
+func (fp *FrontendPluginBase) handleModuleDefaults() {
+	if fp.Module != "" {
+		return
+	}
+
+	if fp.StaticRoot != "" {
+		fp.Module = path.Join("plugins", fp.Id, "module")
+		return
+	}
+
+	fp.Module = path.Join("app/plugins", fp.Type, fp.Id, "module")
+}
+
+func evalRelativePluginUrlPath(pathStr string, pluginId string) string {
+	u, _ := url.Parse(pathStr)
+	if u.IsAbs() {
+		return pathStr
+	}
+	return path.Join("public/plugins", pluginId, pathStr)
+}

+ 70 - 22
pkg/plugins/models.go

@@ -1,26 +1,74 @@
 package plugins
 
-type DataSourcePlugin struct {
-	Type               string                 `json:"type"`
-	Name               string                 `json:"name"`
-	ServiceName        string                 `json:"serviceName"`
-	Module             string                 `json:"module"`
-	Partials           map[string]interface{} `json:"partials"`
-	DefaultMatchFormat string                 `json:"defaultMatchFormat"`
-	Annotations        bool                   `json:"annotations"`
-	Metrics            bool                   `json:"metrics"`
-	BuiltIn            bool                   `json:"builtIn"`
-	StaticRootConfig   *StaticRootConfig      `json:"staticRoot"`
-}
-
-type PanelPlugin struct {
-	Type             string            `json:"type"`
-	Name             string            `json:"name"`
-	Module           string            `json:"module"`
-	StaticRootConfig *StaticRootConfig `json:"staticRoot"`
-}
-
-type StaticRootConfig struct {
+import (
+	"encoding/json"
+
+	"github.com/grafana/grafana/pkg/models"
+)
+
+type PluginLoader interface {
+	Load(decoder *json.Decoder, pluginDir string) error
+}
+
+type PluginBase struct {
+	Type      string     `json:"type"`
+	Name      string     `json:"name"`
+	Id        string     `json:"id"`
+	App       string     `json:"app"`
+	Info      PluginInfo `json:"info"`
+	PluginDir string     `json:"-"`
+}
+
+type PluginInfo struct {
+	Author      PluginInfoLink   `json:"author"`
+	Description string           `json:"description"`
+	Links       []PluginInfoLink `json:"links"`
+	Logos       PluginLogos      `json:"logos"`
+	Version     string           `json:"version"`
+	Updated     string           `json:"updated"`
+}
+
+type PluginInfoLink struct {
+	Name string `json:"name"`
 	Url  string `json:"url"`
-	Path string `json:"path"`
+}
+
+type PluginLogos struct {
+	Small string `json:"small"`
+	Large string `json:"large"`
+}
+
+type PluginStaticRoute struct {
+	Directory string
+	PluginId  string
+}
+
+type ApiPluginRoute struct {
+	Path            string          `json:"path"`
+	Method          string          `json:"method"`
+	ReqSignedIn     bool            `json:"reqSignedIn"`
+	ReqGrafanaAdmin bool            `json:"reqGrafanaAdmin"`
+	ReqRole         models.RoleType `json:"reqRole"`
+	Url             string          `json:"url"`
+}
+
+type ApiPlugin struct {
+	PluginBase
+	Routes []*ApiPluginRoute `json:"routes"`
+}
+
+type EnabledPlugins struct {
+	Panels      []*PanelPlugin
+	DataSources map[string]*DataSourcePlugin
+	ApiList     []*ApiPlugin
+	Apps        []*AppPlugin
+}
+
+func NewEnabledPlugins() EnabledPlugins {
+	return EnabledPlugins{
+		Panels:      make([]*PanelPlugin, 0),
+		DataSources: make(map[string]*DataSourcePlugin),
+		ApiList:     make([]*ApiPlugin, 0),
+		Apps:        make([]*AppPlugin, 0),
+	}
 }

+ 19 - 0
pkg/plugins/panel_plugin.go

@@ -0,0 +1,19 @@
+package plugins
+
+import "encoding/json"
+
+type PanelPlugin struct {
+	FrontendPluginBase
+}
+
+func (p *PanelPlugin) Load(decoder *json.Decoder, pluginDir string) error {
+	if err := decoder.Decode(&p); err != nil {
+		return err
+	}
+
+	p.PluginDir = pluginDir
+	p.initFrontendPlugin()
+	Panels[p.Id] = p
+
+	return nil
+}

+ 78 - 46
pkg/plugins/plugins.go

@@ -1,12 +1,16 @@
 package plugins
 
 import (
+	"bytes"
 	"encoding/json"
 	"errors"
+	"io"
 	"os"
 	"path"
 	"path/filepath"
+	"reflect"
 	"strings"
+	"text/template"
 
 	"github.com/grafana/grafana/pkg/log"
 	"github.com/grafana/grafana/pkg/setting"
@@ -14,9 +18,12 @@ import (
 )
 
 var (
-	DataSources  map[string]DataSourcePlugin
-	Panels       map[string]PanelPlugin
-	StaticRoutes []*StaticRootConfig
+	DataSources  map[string]*DataSourcePlugin
+	Panels       map[string]*PanelPlugin
+	ApiPlugins   map[string]*ApiPlugin
+	StaticRoutes []*PluginStaticRoute
+	Apps         map[string]*AppPlugin
+	PluginTypes  map[string]interface{}
 )
 
 type PluginScanner struct {
@@ -25,18 +32,45 @@ type PluginScanner struct {
 }
 
 func Init() error {
-	DataSources = make(map[string]DataSourcePlugin)
-	StaticRoutes = make([]*StaticRootConfig, 0)
-	Panels = make(map[string]PanelPlugin)
+	DataSources = make(map[string]*DataSourcePlugin)
+	ApiPlugins = make(map[string]*ApiPlugin)
+	StaticRoutes = make([]*PluginStaticRoute, 0)
+	Panels = make(map[string]*PanelPlugin)
+	Apps = make(map[string]*AppPlugin)
+	PluginTypes = map[string]interface{}{
+		"panel":      PanelPlugin{},
+		"datasource": DataSourcePlugin{},
+		"api":        ApiPlugin{},
+		"app":        AppPlugin{},
+	}
 
 	scan(path.Join(setting.StaticRootPath, "app/plugins"))
-	scan(path.Join(setting.PluginsPath))
-	checkExternalPluginPaths()
-
+	checkPluginPaths()
+	// checkDependencies()
 	return nil
 }
 
-func checkExternalPluginPaths() error {
+// func checkDependencies() {
+// 	for appType, app := range Apps {
+// 		for _, reqPanel := range app.PanelPlugins {
+// 			if _, ok := Panels[reqPanel]; !ok {
+// 				log.Fatal(4, "App %s requires Panel type %s, but it is not present.", appType, reqPanel)
+// 			}
+// 		}
+// 		for _, reqDataSource := range app.DatasourcePlugins {
+// 			if _, ok := DataSources[reqDataSource]; !ok {
+// 				log.Fatal(4, "App %s requires DataSource type %s, but it is not present.", appType, reqDataSource)
+// 			}
+// 		}
+// 		for _, reqApiPlugin := range app.ApiPlugins {
+// 			if _, ok := ApiPlugins[reqApiPlugin]; !ok {
+// 				log.Fatal(4, "App %s requires ApiPlugin type %s, but it is not present.", appType, reqApiPlugin)
+// 			}
+// 		}
+// 	}
+// }
+
+func checkPluginPaths() error {
 	for _, section := range setting.Cfg.Sections() {
 		if strings.HasPrefix(section.Name(), "plugin.") {
 			path := section.Key("path").String()
@@ -87,11 +121,26 @@ func (scanner *PluginScanner) walker(currentPath string, f os.FileInfo, err erro
 	return nil
 }
 
-func addStaticRoot(staticRootConfig *StaticRootConfig, currentDir string) {
-	if staticRootConfig != nil {
-		staticRootConfig.Path = path.Join(currentDir, staticRootConfig.Path)
-		StaticRoutes = append(StaticRoutes, staticRootConfig)
+func interpolatePluginJson(reader io.Reader, pluginCommon *PluginBase) (io.Reader, error) {
+	buf := new(bytes.Buffer)
+	buf.ReadFrom(reader)
+	jsonStr := buf.String() //
+
+	tmpl, err := template.New("json").Parse(jsonStr)
+	if err != nil {
+		return nil, err
+	}
+
+	data := map[string]interface{}{
+		"PluginPublicRoot": "public/plugins/" + pluginCommon.Id,
+	}
+
+	var resultBuffer bytes.Buffer
+	if err := tmpl.ExecuteTemplate(&resultBuffer, "json", data); err != nil {
+		return nil, err
 	}
+
+	return bytes.NewReader(resultBuffer.Bytes()), nil
 }
 
 func (scanner *PluginScanner) loadPluginJson(pluginJsonFilePath string) error {
@@ -104,46 +153,29 @@ func (scanner *PluginScanner) loadPluginJson(pluginJsonFilePath string) error {
 	defer reader.Close()
 
 	jsonParser := json.NewDecoder(reader)
-
-	pluginJson := make(map[string]interface{})
-	if err := jsonParser.Decode(&pluginJson); err != nil {
+	pluginCommon := PluginBase{}
+	if err := jsonParser.Decode(&pluginCommon); err != nil {
 		return err
 	}
 
-	pluginType, exists := pluginJson["pluginType"]
-	if !exists {
-		return errors.New("Did not find pluginType property in plugin.json")
+	if pluginCommon.Id == "" || pluginCommon.Type == "" {
+		return errors.New("Did not find type and id property in plugin.json")
 	}
 
-	if pluginType == "datasource" {
-		p := DataSourcePlugin{}
-		reader.Seek(0, 0)
-		if err := jsonParser.Decode(&p); err != nil {
-			return err
-		}
-
-		if p.Type == "" {
-			return errors.New("Did not find type property in plugin.json")
-		}
-
-		DataSources[p.Type] = p
-		addStaticRoot(p.StaticRootConfig, currentDir)
+	reader.Seek(0, 0)
+	if newReader, err := interpolatePluginJson(reader, &pluginCommon); err != nil {
+		return err
+	} else {
+		jsonParser = json.NewDecoder(newReader)
 	}
 
-	if pluginType == "panel" {
-		p := PanelPlugin{}
-		reader.Seek(0, 0)
-		if err := jsonParser.Decode(&p); err != nil {
-			return err
-		}
-
-		if p.Type == "" {
-			return errors.New("Did not find type property in plugin.json")
-		}
+	var loader PluginLoader
 
-		Panels[p.Type] = p
-		addStaticRoot(p.StaticRootConfig, currentDir)
+	if pluginGoType, exists := PluginTypes[pluginCommon.Type]; !exists {
+		return errors.New("Unkown plugin type " + pluginCommon.Type)
+	} else {
+		loader = reflect.New(reflect.TypeOf(pluginGoType)).Interface().(PluginLoader)
 	}
 
-	return nil
+	return loader.Load(jsonParser, currentDir)
 }

+ 17 - 0
pkg/plugins/plugins_test.go

@@ -18,5 +18,22 @@ func TestPluginScans(t *testing.T) {
 
 		So(err, ShouldBeNil)
 		So(len(DataSources), ShouldBeGreaterThan, 1)
+		So(len(Panels), ShouldBeGreaterThan, 1)
+
+		Convey("Should set module automatically", func() {
+			So(DataSources["graphite"].Module, ShouldEqual, "app/plugins/datasource/graphite/module")
+		})
+	})
+
+	Convey("When reading app plugin definition", t, func() {
+		setting.Cfg = ini.Empty()
+		sec, _ := setting.Cfg.NewSection("plugin.app-test")
+		sec.NewKey("path", "../../tests/app-plugin-json")
+		err := Init()
+
+		So(err, ShouldBeNil)
+		So(len(Apps), ShouldBeGreaterThan, 0)
+		So(Apps["app-example"].Info.Logos.Large, ShouldEqual, "public/plugins/app-example/img/logo_large.png")
 	})
+
 }

+ 75 - 0
pkg/plugins/queries.go

@@ -0,0 +1,75 @@
+package plugins
+
+import (
+	"github.com/grafana/grafana/pkg/bus"
+	m "github.com/grafana/grafana/pkg/models"
+)
+
+func GetOrgAppSettings(orgId int64) (map[string]*m.AppSettings, error) {
+	query := m.GetAppSettingsQuery{OrgId: orgId}
+
+	if err := bus.Dispatch(&query); err != nil {
+		return nil, err
+	}
+
+	orgAppsMap := make(map[string]*m.AppSettings)
+	for _, orgApp := range query.Result {
+		orgAppsMap[orgApp.AppId] = orgApp
+	}
+
+	return orgAppsMap, nil
+}
+
+func GetEnabledPlugins(orgId int64) (*EnabledPlugins, error) {
+	enabledPlugins := NewEnabledPlugins()
+	orgApps, err := GetOrgAppSettings(orgId)
+	if err != nil {
+		return nil, err
+	}
+
+	seenPanels := make(map[string]bool)
+	seenApi := make(map[string]bool)
+
+	for appId, installedApp := range Apps {
+		var app AppPlugin
+		app = *installedApp
+
+		// check if the app is stored in the DB for this org and if so, use the
+		// state stored there.
+		if b, ok := orgApps[appId]; ok {
+			app.Enabled = b.Enabled
+			app.Pinned = b.Pinned
+		}
+
+		if app.Enabled {
+			enabledPlugins.Apps = append(enabledPlugins.Apps, &app)
+		}
+	}
+
+	// add all plugins that are not part of an App.
+	for d, installedDs := range DataSources {
+		if installedDs.App == "" {
+			enabledPlugins.DataSources[d] = installedDs
+		}
+	}
+
+	for p, panel := range Panels {
+		if panel.App == "" {
+			if _, ok := seenPanels[p]; !ok {
+				seenPanels[p] = true
+				enabledPlugins.Panels = append(enabledPlugins.Panels, panel)
+			}
+		}
+	}
+
+	for a, api := range ApiPlugins {
+		if api.App == "" {
+			if _, ok := seenApi[a]; !ok {
+				seenApi[a] = true
+				enabledPlugins.ApiList = append(enabledPlugins.ApiList, api)
+			}
+		}
+	}
+
+	return &enabledPlugins, nil
+}

+ 50 - 0
pkg/services/sqlstore/app_settings.go

@@ -0,0 +1,50 @@
+package sqlstore
+
+import (
+	"time"
+
+	"github.com/grafana/grafana/pkg/bus"
+	m "github.com/grafana/grafana/pkg/models"
+)
+
+func init() {
+	bus.AddHandler("sql", GetAppSettings)
+	bus.AddHandler("sql", UpdateAppSettings)
+}
+
+func GetAppSettings(query *m.GetAppSettingsQuery) error {
+	sess := x.Where("org_id=?", query.OrgId)
+
+	query.Result = make([]*m.AppSettings, 0)
+	return sess.Find(&query.Result)
+}
+
+func UpdateAppSettings(cmd *m.UpdateAppSettingsCmd) error {
+	return inTransaction2(func(sess *session) error {
+		var app m.AppSettings
+
+		exists, err := sess.Where("org_id=? and app_id=?", cmd.OrgId, cmd.AppId).Get(&app)
+		sess.UseBool("enabled")
+		sess.UseBool("pinned")
+		if !exists {
+			app = m.AppSettings{
+				AppId:    cmd.AppId,
+				OrgId:    cmd.OrgId,
+				Enabled:  cmd.Enabled,
+				Pinned:   cmd.Pinned,
+				JsonData: cmd.JsonData,
+				Created:  time.Now(),
+				Updated:  time.Now(),
+			}
+			_, err = sess.Insert(&app)
+			return err
+		} else {
+			app.Updated = time.Now()
+			app.Enabled = cmd.Enabled
+			app.JsonData = cmd.JsonData
+			app.Pinned = cmd.Pinned
+			_, err = sess.Id(app.Id).Update(&app)
+			return err
+		}
+	})
+}

+ 9 - 7
pkg/services/sqlstore/migrations/plugin_bundle.go → pkg/services/sqlstore/migrations/app_settings.go

@@ -2,25 +2,27 @@ package migrations
 
 import . "github.com/grafana/grafana/pkg/services/sqlstore/migrator"
 
-func addPluginBundleMigration(mg *Migrator) {
+func addAppSettingsMigration(mg *Migrator) {
 
-	var pluginBundleV1 = Table{
-		Name: "plugin_bundle",
+	appSettingsV1 := Table{
+		Name: "app_settings",
 		Columns: []*Column{
 			{Name: "id", Type: DB_BigInt, IsPrimaryKey: true, IsAutoIncrement: true},
 			{Name: "org_id", Type: DB_BigInt, Nullable: true},
-			{Name: "type", Type: DB_NVarchar, Length: 255, Nullable: false},
+			{Name: "app_id", Type: DB_NVarchar, Length: 255, Nullable: false},
 			{Name: "enabled", Type: DB_Bool, Nullable: false},
+			{Name: "pinned", Type: DB_Bool, Nullable: false},
 			{Name: "json_data", Type: DB_Text, Nullable: true},
 			{Name: "created", Type: DB_DateTime, Nullable: false},
 			{Name: "updated", Type: DB_DateTime, Nullable: false},
 		},
 		Indices: []*Index{
-			{Cols: []string{"org_id", "type"}, Type: UniqueIndex},
+			{Cols: []string{"org_id", "app_id"}, Type: UniqueIndex},
 		},
 	}
-	mg.AddMigration("create plugin_bundle table v1", NewAddTableMigration(pluginBundleV1))
+
+	mg.AddMigration("create app_settings table v1", NewAddTableMigration(appSettingsV1))
 
 	//-------  indexes ------------------
-	addTableIndicesMigrations(mg, "v1", pluginBundleV1)
+	addTableIndicesMigrations(mg, "v3", appSettingsV1)
 }

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

@@ -18,7 +18,7 @@ func AddMigrations(mg *Migrator) {
 	addApiKeyMigrations(mg)
 	addDashboardSnapshotMigrations(mg)
 	addQuotaMigration(mg)
-	addPluginBundleMigration(mg)
+	addAppSettingsMigration(mg)
 	addSessionMigration(mg)
 	addPlaylistMigrations(mg)
 }

+ 0 - 46
pkg/services/sqlstore/plugin_bundle.go

@@ -1,46 +0,0 @@
-package sqlstore
-
-import (
-	"time"
-
-	"github.com/grafana/grafana/pkg/bus"
-	m "github.com/grafana/grafana/pkg/models"
-)
-
-func init() {
-	bus.AddHandler("sql", GetPluginBundles)
-	bus.AddHandler("sql", UpdatePluginBundle)
-}
-
-func GetPluginBundles(query *m.GetPluginBundlesQuery) error {
-	sess := x.Where("org_id=?", query.OrgId)
-
-	query.Result = make([]*m.PluginBundle, 0)
-	return sess.Find(&query.Result)
-}
-
-func UpdatePluginBundle(cmd *m.UpdatePluginBundleCmd) error {
-	return inTransaction2(func(sess *session) error {
-		var bundle m.PluginBundle
-
-		exists, err := sess.Where("org_id=? and type=?", cmd.OrgId, cmd.Type).Get(&bundle)
-		sess.UseBool("enabled")
-		if !exists {
-			bundle = m.PluginBundle{
-				Type:     cmd.Type,
-				OrgId:    cmd.OrgId,
-				Enabled:  cmd.Enabled,
-				JsonData: cmd.JsonData,
-				Created:  time.Now(),
-				Updated:  time.Now(),
-			}
-			_, err = sess.Insert(&bundle)
-			return err
-		} else {
-			bundle.Enabled = cmd.Enabled
-			bundle.JsonData = cmd.JsonData
-			_, err = sess.Id(bundle.Id).Update(&bundle)
-			return err
-		}
-	})
-}

+ 0 - 6
public/app/core/controllers/all.js

@@ -1,9 +1,3 @@
-// import grafanaCtrl from './grafana_ctrl';
-//
-// import * as asd from './sidemenu_ctrl';
-//
-// export {grafanaCtrl};
-
 define([
   './grafana_ctrl',
   './search_ctrl',

+ 2 - 1
public/app/core/controllers/sidemenu_ctrl.js

@@ -19,7 +19,8 @@ function (angular, _, $, coreModule, config) {
         $scope.mainLinks.push({
           text: item.text,
           icon: item.icon,
-          href: $scope.getUrl(item.href)
+          img: item.img,
+          url: $scope.getUrl(item.url)
         });
       });
     };

+ 3 - 2
public/app/core/directives/misc.js

@@ -62,12 +62,13 @@ function (angular, coreModule, kbn) {
         var label = '<label for="' + scope.$id + model + '" class="checkbox-label">' +
           text + tip + '</label>';
 
-        var template = '<input class="cr1" id="' + scope.$id + model + '" type="checkbox" ' +
+        var template =
+          '<input class="cr1" id="' + scope.$id + model + '" type="checkbox" ' +
           '       ng-model="' + model + '"' + ngchange +
           '       ng-checked="' + model + '"></input>' +
           ' <label for="' + scope.$id + model + '" class="cr1"></label>';
 
-        template = label + template;
+        template = template + label;
         elem.replaceWith($compile(angular.element(template))(scope));
       }
     };

+ 15 - 12
public/app/core/routes/all.js

@@ -10,6 +10,7 @@ define([
     $locationProvider.html5Mode(true);
 
     var loadOrgBundle = new BundleLoader.BundleLoader('app/features/org/all');
+    var loadAppsBundle = new BundleLoader.BundleLoader('app/features/apps/all');
 
     $routeProvider
       .when('/', {
@@ -41,17 +42,17 @@ define([
         controller : 'DashboardImportCtrl',
       })
       .when('/datasources', {
-        templateUrl: 'app/features/org/partials/datasources.html',
+        templateUrl: 'app/features/datasources/partials/list.html',
         controller : 'DataSourcesCtrl',
         resolve: loadOrgBundle,
       })
       .when('/datasources/edit/:id', {
-        templateUrl: 'app/features/org/partials/datasourceEdit.html',
+        templateUrl: 'app/features/datasources/partials/edit.html',
         controller : 'DataSourceEditCtrl',
         resolve: loadOrgBundle,
       })
       .when('/datasources/new', {
-        templateUrl: 'app/features/org/partials/datasourceEdit.html',
+        templateUrl: 'app/features/datasources/partials/edit.html',
         controller : 'DataSourceEditCtrl',
         resolve: loadOrgBundle,
       })
@@ -131,15 +132,17 @@ define([
         templateUrl: 'app/partials/reset_password.html',
         controller : 'ResetPasswordCtrl',
       })
-      .when('/plugins', {
-        templateUrl: 'app/features/org/partials/plugins.html',
-        controller: 'PluginsCtrl',
-        resolve: loadOrgBundle,
-      })
-      .when('/plugins/edit/:type', {
-        templateUrl: 'app/features/org/partials/pluginEdit.html',
-        controller: 'PluginEditCtrl',
-        resolve: loadOrgBundle,
+      .when('/apps', {
+        templateUrl: 'app/features/apps/partials/list.html',
+        controller: 'AppListCtrl',
+        controllerAs: 'ctrl',
+        resolve: loadAppsBundle,
+      })
+      .when('/apps/edit/:appId', {
+        templateUrl: 'app/features/apps/partials/edit.html',
+        controller: 'AppEditCtrl',
+        controllerAs: 'ctrl',
+        resolve: loadAppsBundle,
       })
       .when('/global-alerts', {
         templateUrl: 'app/features/dashboard/partials/globalAlerts.html',

+ 4 - 0
public/app/core/services/alert_srv.js

@@ -46,6 +46,10 @@ function (angular, _, coreModule) {
         }, timeout);
       }
 
+      if (!$rootScope.$$phase) {
+        $rootScope.$digest();
+      }
+
       return(newAlert);
     };
 

+ 15 - 6
public/app/core/services/datasource_srv.js

@@ -7,7 +7,7 @@ define([
 function (angular, _, coreModule, config) {
   'use strict';
 
-  coreModule.default.service('datasourceSrv', function($q, $injector) {
+  coreModule.default.service('datasourceSrv', function($q, $injector, $rootScope) {
     var self = this;
 
     this.init = function() {
@@ -58,18 +58,27 @@ function (angular, _, coreModule, config) {
       }
 
       var deferred = $q.defer();
-
       var pluginDef = dsConfig.meta;
 
-      System.import(pluginDef.module).then(function() {
-        var AngularService = $injector.get(pluginDef.serviceName);
-        var instance = new AngularService(dsConfig, pluginDef);
+      System.import(pluginDef.module).then(function(plugin) {
+        // check if its in cache now
+        if (self.datasources[name]) {
+          deferred.resolve(self.datasources[name]);
+          return;
+        }
+
+        // plugin module needs to export a constructor function named Datasource
+        if (!plugin.Datasource) {
+          throw "Plugin module is missing Datasource constructor";
+        }
+
+        var instance = $injector.instantiate(plugin.Datasource, {instanceSettings: dsConfig});
         instance.meta = pluginDef;
         instance.name = name;
         self.datasources[name] = instance;
         deferred.resolve(instance);
       }).catch(function(err) {
-        console.log('Failed to load data source: ' + err);
+        $rootScope.appEvent('alert-error', [dsConfig.name + ' plugin failed', err.toString()]);
       });
 
       return deferred.promise;

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

@@ -44,7 +44,7 @@
 				<table class="grafana-options-table">
 					<tr ng-repeat="annotation in annotations">
 						<td style="width:90%">
-                            <i class="fa fa-bolt" style="color:{{annotation.iconColor}}"></i> &nbsp;
+							<i class="fa fa-bolt" style="color:{{annotation.iconColor}}"></i> &nbsp;
 							{{annotation.name}}
 						</td>
 						<td style="width: 1%"><i ng-click="_.move(annotations,$index,$index-1)" ng-hide="$first" class="pointer fa fa-arrow-up"></i></td>

+ 2 - 0
public/app/features/apps/all.ts

@@ -0,0 +1,2 @@
+import './edit_ctrl';
+import './list_ctrl';

+ 43 - 0
public/app/features/apps/app_srv.ts

@@ -0,0 +1,43 @@
+///<reference path="../../headers/common.d.ts" />
+
+import _ from 'lodash';
+import angular from 'angular';
+
+export class AppSrv {
+  apps: any = {};
+
+  /** @ngInject */
+  constructor(
+    private $rootScope,
+    private $timeout,
+    private $q,
+    private backendSrv) {
+  }
+
+  get(type) {
+    return this.getAll().then(() => {
+      return this.apps[type];
+    });
+  }
+
+  getAll() {
+    if (!_.isEmpty(this.apps)) {
+      return this.$q.when(this.apps);
+    }
+
+    return this.backendSrv.get('api/org/apps').then(results => {
+      return results.reduce((prev, current) => {
+        prev[current.type] = current;
+        return prev;
+      }, this.apps);
+    });
+  }
+
+  update(app) {
+    return this.backendSrv.post('api/org/apps', app).then(resp => {
+
+    });
+  }
+}
+
+angular.module('grafana.services').service('appSrv', AppSrv);

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

@@ -0,0 +1,44 @@
+///<reference path="../../headers/common.d.ts" />
+
+import config from 'app/core/config';
+import angular from 'angular';
+import _ from 'lodash';
+
+export class AppEditCtrl {
+  appModel: any;
+
+  /** @ngInject */
+  constructor(private backendSrv: any, private $routeParams: any) {}
+
+  init() {
+    this.appModel = {};
+    this.backendSrv.get(`/api/org/apps/${this.$routeParams.appId}/settings`).then(result => {
+      this.appModel = result;
+    });
+  }
+
+  update(options) {
+    var updateCmd = _.extend({
+      appId: this.appModel.appId,
+      orgId: this.appModel.orgId,
+      enabled: this.appModel.enabled,
+      pinned: this.appModel.pinned,
+      jsonData: this.appModel.jsonData,
+    }, options);
+
+    this.backendSrv.post(`/api/org/apps/${this.$routeParams.appId}/settings`, updateCmd).then(function() {
+      window.location.href = window.location.href;
+    });
+  }
+
+  toggleEnabled() {
+    this.update({enabled: this.appModel.enabled});
+  }
+
+  togglePinned() {
+    this.update({pinned: this.appModel.pinned});
+  }
+}
+
+angular.module('grafana.controllers').controller('AppEditCtrl', AppEditCtrl);
+

+ 19 - 0
public/app/features/apps/list_ctrl.ts

@@ -0,0 +1,19 @@
+///<reference path="../../headers/common.d.ts" />
+
+import config = require('app/core/config');
+import angular from 'angular';
+
+export class AppListCtrl {
+  apps: any[];
+
+  /** @ngInject */
+  constructor(private backendSrv: any) {}
+
+  init() {
+    this.backendSrv.get('api/org/apps').then(apps => {
+      this.apps = apps;
+    });
+  }
+}
+
+angular.module('grafana.controllers').controller('AppListCtrl', AppListCtrl);

+ 102 - 0
public/app/features/apps/partials/edit.html

@@ -0,0 +1,102 @@
+<topnav title="Apps" icon="fa fa-fw fa-cubes" subnav="true">
+	<ul class="nav">
+		<li ><a href="apps">Overview</a></li>
+		<li class="active" ><a href="apps/edit/{{ctrl.current.type}}">Edit</a></li>
+	</ul>
+</topnav>
+
+<div class="page-container" style="background: transparent; border: 0;">
+	<div class="apps-side-box">
+		<div class="apps-side-box-logo" >
+			<img src="{{ctrl.appModel.info.logos.large}}">
+		</div>
+		<ul class="app-side-box-links">
+			<li>
+				By <a href="{{ctrl.appModel.info.author.url}}" class="external-link" target="_blank">{{ctrl.appModel.info.author.name}}</a>
+			</li>
+			<li ng-repeat="link in ctrl.appModel.info.links">
+				<a href="{{link.url}}" class="external-link" target="_blank">{{link.name}}</a>
+			</li>
+		</ul>
+	</div>
+
+  <div class="page-wide-margined" ng-init="ctrl.init()">
+		<h1>
+			{{ctrl.appModel.name}}
+		</h1>
+		<em>
+			{{ctrl.appModel.info.description}}<br>
+			<span style="small">
+			Version: {{ctrl.appModel.info.version}} &nbsp; &nbsp; Updated: {{ctrl.appModel.info.updated}}
+		</span>
+
+		</em>
+		<br><br>
+
+		<div class="form-inline">
+			<editor-checkbox text="Enabled" model="ctrl.appModel.enabled" change="ctrl.toggleEnabled()"></editor-checkbox>
+			&nbsp; &nbsp; &nbsp;
+			<editor-checkbox text="Pinned" model="ctrl.appModel.pinned" change="ctrl.togglePinned()"></editor-checkbox>
+		</div>
+
+		<section class="simple-box">
+			<h3 class="simple-box-header">Included with app:</h3>
+			<div class="flex-container">
+				<div class="simple-box-body simple-box-column">
+					<div class="simple-box-column-header">
+						<i class="fa fa-th-large"></i>
+						Dashboards
+					</div>
+					<ul>
+						<li><em class="small">None</em></li>
+					</ul>
+				</div>
+				<div class="simple-box-body simple-box-column">
+					<div class="simple-box-column-header">
+						<i class="fa fa-line-chart"></i>
+						Panels
+					</div>
+					<ul>
+						<li><em class="small">None</em></li>
+					</ul>
+				</div>
+				<div class="simple-box-body simple-box-column">
+					<div class="simple-box-column-header">
+						<i class="fa fa-database"></i>
+						Datasources
+					</div>
+					<ul>
+						<li><em class="small">None</em></li>
+					</ul>
+				</div>
+				<div class="simple-box-body simple-box-column">
+					<div class="simple-box-column-header">
+						<i class="fa fa-files-o"></i>
+						Pages
+					</div>
+					<ul>
+						<li ng-repeat="page in ctrl.appModel.pages">
+							<a href="{{page.url}}" class="external-link">{{page.name}}</a>
+						</li>
+					</ul>
+				</div>
+
+			</div>
+		</section>
+
+		<section class="simple-box">
+			<h3 class="simple-box-header">Dependencies:</h3>
+			<div class="simple-box-body">
+				Grafana 2.6.x
+			</div>
+		</section>
+
+		<section class="simple-box">
+			<h3 class="simple-box-header">Configuration:</h3>
+			<div class="simple-box-body">
+			</div>
+		</section>
+
+		<app-config-loader></app-config-loader>
+	</div>
+</div>

+ 51 - 0
public/app/features/apps/partials/list.html

@@ -0,0 +1,51 @@
+<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>
+
+<div class="page-container" style="background: transparent; border: 0;">
+  <div class="page-wide" ng-init="ctrl.init()">
+    <h2>Apps</h2>
+
+		<div ng-if="!ctrl.apps">
+			<em>No apps defined</em>
+		</div>
+
+		<ul class="filter-list">
+      <li ng-repeat="app in ctrl.apps">
+        <ul class="filter-list-card">
+					<li class="filter-list-card-image">
+						<img ng-src="{{app.info.logos.small}}">
+					</li>
+          <li>
+            <div class="filter-list-card-controls">
+              <div class="filter-list-card-config">
+								<a href="apps/edit/{{app.appId}}">
+									<i class="fa fa-cog"></i>
+								</a>
+              </div>
+            </div>
+						<span class="filter-list-card-title">
+							<a href="apps/edit/{{app.appId}}">
+								{{app.name}}
+							</a>
+							&nbsp; &nbsp;
+							<span class="label label-info" ng-if="app.enabled">
+								Enabled
+							</span>
+							&nbsp;
+							<span class="label label-info" ng-if="app.pinned">
+								Pinned
+							</span>
+
+						</span>
+            <span class="filter-list-card-status">
+              <span class="filter-list-card-state">Dashboards: 1</span>
+            </span>
+          </li>
+        </ul>
+      </li>
+		</ul>
+	</div>
+</div>

+ 0 - 8
public/app/features/dashboard/dashboardCtrl.js

@@ -106,14 +106,6 @@ function (angular, $, config, moment) {
       };
     };
 
-    $scope.panelEditorPath = function(type) {
-      return 'app/' + config.panels[type].path + '/editor.html';
-    };
-
-    $scope.pulldownEditorPath = function(type) {
-      return 'app/panels/'+type+'/editor.html';
-    };
-
     $scope.showJsonEditor = function(evt, options) {
       var editScope = $rootScope.$new();
       editScope.object = options.object;

+ 4 - 0
public/app/features/datasources/all.js

@@ -0,0 +1,4 @@
+define([
+  './list_ctrl',
+  './edit_ctrl',
+], function () {});

+ 5 - 23
public/app/features/org/datasourceEditCtrl.js → public/app/features/datasources/edit_ctrl.js

@@ -9,26 +9,14 @@ function (angular, _, config) {
   var module = angular.module('grafana.controllers');
   var datasourceTypes = [];
 
-  module.controller('DataSourceEditCtrl', function($scope, $q, backendSrv, $routeParams, $location, datasourceSrv) {
+  module.directive('datasourceHttpSettings', function() {
+    return {templateUrl: 'app/features/datasources/partials/http_settings.html'};
+  });
 
-    $scope.httpConfigPartialSrc = 'app/features/org/partials/datasourceHttpConfig.html';
+  module.controller('DataSourceEditCtrl', function($scope, $q, backendSrv, $routeParams, $location, datasourceSrv) {
 
     var defaults = {name: '', type: 'graphite', url: '', access: 'proxy', jsonData: {}};
 
-    $scope.indexPatternTypes = [
-      {name: 'No pattern',  value: undefined},
-      {name: 'Hourly',      value: 'Hourly',  example: '[logstash-]YYYY.MM.DD.HH'},
-      {name: 'Daily',       value: 'Daily',   example: '[logstash-]YYYY.MM.DD'},
-      {name: 'Weekly',      value: 'Weekly',  example: '[logstash-]GGGG.WW'},
-      {name: 'Monthly',     value: 'Monthly', example: '[logstash-]YYYY.MM'},
-      {name: 'Yearly',      value: 'Yearly',  example: '[logstash-]YYYY'},
-    ];
-
-    $scope.esVersions = [
-      {name: '1.x', value: 1},
-      {name: '2.x', value: 2},
-    ];
-
     $scope.init = function() {
       $scope.isNew = true;
       $scope.datasources = [];
@@ -59,7 +47,7 @@ function (angular, _, config) {
       backendSrv.get('/api/datasources/' + id).then(function(ds) {
         $scope.isNew = false;
         $scope.current = ds;
-        $scope.typeChanged();
+        return $scope.typeChanged();
       });
     };
 
@@ -127,12 +115,6 @@ function (angular, _, config) {
       }
     };
 
-    $scope.indexPatternTypeChanged = function() {
-      var def = _.findWhere($scope.indexPatternTypes, {value: $scope.current.jsonData.interval});
-      $scope.current.database = def.example || 'es-index-name';
-    };
-
     $scope.init();
-
   });
 });

+ 0 - 0
public/app/features/org/datasourcesCtrl.js → public/app/features/datasources/list_ctrl.js


+ 1 - 1
public/app/features/org/partials/datasourceEdit.html → public/app/features/datasources/partials/edit.html

@@ -42,7 +42,7 @@
 				<div class="clearfix"></div>
 			</div>
 
-			<div ng-include="datasourceMeta.partials.config" ng-if="datasourceMeta.partials.config"></div>
+			<datasource-custom-settings-view ds-meta="datasourceMeta" current="current"></datasource-custom-settings-view>
 
 			<div ng-if="testing" style="margin-top: 25px">
 				<h5 ng-show="!testing.done">Testing.... <i class="fa fa-spiner fa-spin"></i></h5>

+ 2 - 0
public/app/features/org/partials/datasourceHttpConfig.html → public/app/features/datasources/partials/http_settings.html

@@ -53,3 +53,5 @@
 		<div class="clearfix"></div>
 	</div>
 </div>
+
+<br>

+ 0 - 0
public/app/features/org/partials/datasources.html → public/app/features/datasources/partials/list.html


+ 1 - 6
public/app/features/org/all.js

@@ -1,13 +1,8 @@
 define([
-  './datasourcesCtrl',
-  './datasourceEditCtrl',
   './orgUsersCtrl',
   './newOrgCtrl',
   './userInviteCtrl',
   './orgApiKeysCtrl',
   './orgDetailsCtrl',
-  './pluginsCtrl',
-  './pluginEditCtrl',
-  './plugin_srv',
-  './plugin_directive',
+  '../datasources/all',
 ], function () {});

+ 0 - 3
public/app/features/org/partials/pluginConfigCore.html

@@ -1,3 +0,0 @@
-<div>
-{{current.type}} plugin does not have any additional config.
-</div>

+ 0 - 42
public/app/features/org/partials/pluginEdit.html

@@ -1,42 +0,0 @@
-<topnav title="Plugins" icon="fa fa-fw fa-cubes" subnav="true">
-	<ul class="nav">
-		<li ><a href="plugins">Overview</a></li>
-		<li class="active" ><a href="plugins/edit/{{current.type}}">Edit</a></li>
-	</ul>
-</topnav>
-
-<div class="page-container">
-	<div class="page">
-		<h2>Edit Plugin</h2>
-
-
-		<form name="editForm">
-			<div class="tight-form">
-				<ul class="tight-form-list">
-					<li class="tight-form-item" style="width: 80px">
-						Type
-					</li>
-					<li>
-						<li>
-							<input type="text" disabled="disabled" class="input-xlarge tight-form-input" ng-model="current.type">
-						</li>
-					</li>
-					<li class="tight-form-item">
-						Default&nbsp;
-						<input class="cr1" id="current.enabled" type="checkbox" ng-model="current.enabled" ng-checked="current.enabled">
-						<label for="current.enabled" class="cr1"></label>
-					</li>
-				</ul>
-				<div class="clearfix"></div>
-			</div>
-			<br>
-			<plugin-config-loader plugin="current"></plugin-config-loader>
-			<div class="pull-right" style="margin-top: 35px">
-				<button type="submit" class="btn btn-success" ng-click="update()">Save</button>
-				<a class="btn btn-inverse" href="plugins">Cancel</a>
-			</div>
-			<br>
-		</form>
-
-	</div>
-</div>

+ 0 - 41
public/app/features/org/partials/plugins.html

@@ -1,41 +0,0 @@
-<topnav title="Plugins" icon="fa fa-fw fa-cubes" subnav="true">
-	<ul class="nav">
-		<li class="active" ><a href="plugins">Overview</a></li>
-	</ul>
-</topnav>
-
-<div class="page-container">
-	<div class="page">
-		<h2>Plugins</h2>
-
-		<div ng-if="!plugins">
-			<em>No plugins defined</em>
-		</div>
-
-		<table class="grafana-options-table" ng-if="plugins">
-			<tr>
-				<td><strong>Type</strong></td>
-				<td></td>
-				<td></td>
-			</tr>
-			<tr ng-repeat="(type, p) in plugins">
-				<td style="width:1%">
-					<i class="fa fa-cubes"></i> &nbsp;
-					{{p.type}}
-				</td>
-				<td style="width: 1%">
-					<a href="plugins/edit/{{p.type}}" class="btn btn-inverse btn-mini">
-						<i class="fa fa-edit"></i>
-						Edit
-					</a>
-				</td>
-				<td style="width: 1%">
-					Enabled&nbsp;
-					<input  id="p.enabled" type="checkbox" ng-model="p.enabled" ng-checked="p.enabled" ng-change="update(p)">
-					<label for="p.enabled"></label>
-				</td>
-			</tr>
-		</table>
-
-	</div>
-</div>

+ 0 - 35
public/app/features/org/pluginEditCtrl.js

@@ -1,35 +0,0 @@
-define([
-  'angular',
-  'lodash',
-  'app/core/config',
-],
-function (angular, _, config) {
-  'use strict';
-
-  var module = angular.module('grafana.controllers');
-
-  module.controller('PluginEditCtrl', function($scope, pluginSrv, $routeParams) {
-    $scope.init = function() {
-      $scope.current = {};
-      $scope.getPlugins();
-    };
-
-    $scope.getPlugins = function() {
-      pluginSrv.get($routeParams.type).then(function(result) {
-        $scope.current = _.clone(result);
-      });
-    };
-
-    $scope.update = function() {
-      $scope._update();
-    };
-
-    $scope._update = function() {
-      pluginSrv.update($scope.current).then(function() {
-        window.location.href = config.appSubUrl + "plugins";
-      });
-    };
-
-    $scope.init();
-  });
-});

+ 0 - 47
public/app/features/org/plugin_directive.js

@@ -1,47 +0,0 @@
-define([
-  'angular',
-],
-function (angular) {
-  'use strict';
-
-  var module = angular.module('grafana.directives');
-
-  module.directive('pluginConfigLoader', function($compile) {
-    return {
-      restrict: 'E',
-      link: function(scope, elem) {
-        var directive = 'grafana-plugin-core';
-        //wait for the parent scope to be applied.
-        scope.$watch("current", function(newVal) {
-          if (newVal) {
-            if (newVal.module) {
-              directive = 'grafana-plugin-'+newVal.type;
-            }
-            scope.require([newVal.module], function () {
-              var panelEl = angular.element(document.createElement(directive));
-              elem.append(panelEl);
-              $compile(panelEl)(scope);
-            });
-          }
-        });
-      }
-    };
-  });
-
-  module.directive('grafanaPluginCore', function() {
-    return {
-      restrict: 'E',
-      templateUrl: 'app/features/org/partials/pluginConfigCore.html',
-      transclude: true,
-      link: function(scope) {
-        scope.update = function() {
-          //Perform custom save events to the plugins own backend if needed.
-
-          // call parent update to commit the change to the plugin object.
-          // this will cause the page to reload.
-          scope._update();
-        };
-      }
-    };
-  });
-});

+ 0 - 58
public/app/features/org/plugin_srv.js

@@ -1,58 +0,0 @@
-define([
-  'angular',
-  'lodash',
-],
-function (angular, _) {
-  'use strict';
-
-  var module = angular.module('grafana.services');
-
-  module.service('pluginSrv', function($rootScope, $timeout, $q, backendSrv) {
-    var self = this;
-    this.init = function() {
-      console.log("pluginSrv init");
-      this.plugins = {};
-    };
-
-    this.get = function(type) {
-      return $q(function(resolve) {
-        if (type in self.plugins) {
-          return resolve(self.plugins[type]);
-        }
-        backendSrv.get('/api/plugins').then(function(results) {
-          _.forEach(results, function(p) {
-            self.plugins[p.type] = p;
-          });
-          return resolve(self.plugins[type]);
-        });
-      });
-    };
-
-    this.getAll = function() {
-      return $q(function(resolve) {
-        if (!_.isEmpty(self.plugins)) {
-          return resolve(self.plugins);
-        }
-        backendSrv.get('api/plugins').then(function(results) {
-          _.forEach(results, function(p) {
-            self.plugins[p.type] = p;
-          });
-          return resolve(self.plugins);
-        });
-      });
-    };
-
-    this.update = function(plugin) {
-      return $q(function(resolve, reject) {
-        backendSrv.post('/api/plugins', plugin).then(function(resp) {
-          self.plugins[plugin.type] = plugin;
-          resolve(resp);
-        }, function(resp) {
-          reject(resp);
-        });
-      });
-    };
-
-    this.init();
-  });
-});

+ 0 - 33
public/app/features/org/pluginsCtrl.js

@@ -1,33 +0,0 @@
-define([
-  'angular',
-  'app/core/config',
-],
-function (angular, config) {
-  'use strict';
-
-  var module = angular.module('grafana.controllers');
-
-  module.controller('PluginsCtrl', function($scope, $location, pluginSrv) {
-
-    $scope.init = function() {
-      $scope.plugins = {};
-      $scope.getPlugins();
-    };
-
-    $scope.getPlugins = function() {
-      pluginSrv.getAll().then(function(result) {
-        console.log(result);
-        $scope.plugins = result;
-      });
-    };
-
-    $scope.update = function(plugin) {
-      pluginSrv.update(plugin).then(function() {
-        window.location.href = config.appSubUrl + $location.path();
-      });
-    };
-
-    $scope.init();
-
-  });
-});

+ 43 - 16
public/app/features/panel/panel_directive.js

@@ -43,6 +43,33 @@ function (angular, $, config) {
     };
   });
 
+  module.directive('datasourceCustomSettingsView', function($compile) {
+    return {
+      restrict: 'E',
+      scope: {
+        dsMeta: "=",
+        current: "=",
+      },
+      link: function(scope, elem) {
+        scope.$watch("dsMeta.module", function() {
+          if (!scope.dsMeta) {
+            return;
+          }
+
+          System.import(scope.dsMeta.module).then(function() {
+            elem.empty();
+            var panelEl = angular.element(document.createElement('datasource-custom-settings-view-' + scope.dsMeta.id));
+            elem.append(panelEl);
+            $compile(panelEl)(scope);
+          }).catch(function(err) {
+            console.log('Failed to load plugin:', err);
+            scope.appEvent('alert-error', ['Plugin Load Error', 'Failed to load plugin ' + scope.dsMeta.id + ', ' + err]);
+          });
+        });
+      }
+    };
+  });
+
   module.service('dynamicDirectiveSrv', function($compile, $parse, datasourceSrv) {
     var self = this;
 
@@ -62,12 +89,26 @@ function (angular, $, config) {
 
         editorScope = options.scope.$new();
         datasourceSrv.get(newVal).then(function(ds) {
-          self.addDirective(options, ds.meta.type, editorScope);
+          self.addDirective(options, ds.meta.id, editorScope);
         });
       });
     };
   });
 
+  module.directive('datasourceEditorView', function(dynamicDirectiveSrv) {
+    return {
+      restrict: 'E',
+      link: function(scope, elem, attrs) {
+        dynamicDirectiveSrv.define({
+          datasourceProperty: attrs.datasource,
+          name: attrs.name,
+          scope: scope,
+          parentElem: elem,
+        });
+      }
+    };
+  });
+
   module.directive('queryEditorLoader', function($compile, $parse, datasourceSrv) {
     return {
       restrict: 'E',
@@ -90,7 +131,7 @@ function (angular, $, config) {
               scope.target.refId = 'A';
             }
 
-            var panelEl = angular.element(document.createElement('metric-query-editor-' + ds.meta.type));
+            var panelEl = angular.element(document.createElement('metric-query-editor-' + ds.meta.id));
             elem.append(panelEl);
             $compile(panelEl)(editorScope);
           });
@@ -99,20 +140,6 @@ function (angular, $, config) {
     };
   });
 
-  module.directive('datasourceEditorView', function(dynamicDirectiveSrv) {
-    return {
-      restrict: 'E',
-      link: function(scope, elem, attrs) {
-        dynamicDirectiveSrv.define({
-          datasourceProperty: attrs.datasource,
-          name: attrs.name,
-          scope: scope,
-          parentElem: elem,
-        });
-      }
-    };
-  });
-
   module.directive('panelResizer', function($rootScope) {
     return {
       restrict: 'E',

+ 5 - 9
public/app/partials/sidemenu.html

@@ -34,13 +34,6 @@
 			</ul>
 		</li>
 
-		<li ng-if="!contextSrv.isSignedIn">
-			<a href="login" class="sidemenu-item" target="_self">
-				<span class="icon-circle sidemenu-icon"><i class="fa fa-fw fa-sign-in"></i></span>
-				<span class="sidemenu-item-text">Sign in</span>
-			</a>
-		</li>
-
 		<li class="sidemenu-system-section" ng-if="systemSection">
 			<div class="sidemenu-system-section-inner">
 				<i class="fa fa-fw fa-cubes"></i>
@@ -52,8 +45,11 @@
 		</li>
 
 		<li ng-repeat="item in mainLinks">
-			<a href="{{item.href}}" class="sidemenu-item sidemenu-main-link" target="{{item.target}}">
-				<span class="icon-circle sidemenu-icon"><i class="{{item.icon}}"></i></span>
+			<a href="{{item.url}}" class="sidemenu-item sidemenu-main-link" target="{{item.target}}">
+				<span class="icon-circle sidemenu-icon">
+					<i class="{{item.icon}}" ng-show="item.icon"></i>
+					<img ng-src="{{item.img}}" ng-show="item.img">
+				</span>
 				<span class="sidemenu-item-text">{{item.text}}</span>
 			</a>
 		</li>

+ 3 - 0
public/app/plugins/datasource/cloudwatch/datasource.d.ts

@@ -0,0 +1,3 @@
+declare var Datasource: any;
+export default Datasource;
+

+ 24 - 29
public/app/plugins/datasource/cloudwatch/datasource.js

@@ -4,24 +4,19 @@ define([
   'moment',
   'app/core/utils/datemath',
   './query_ctrl',
-  './directives',
 ],
 function (angular, _, moment, dateMath) {
   'use strict';
 
-  var module = angular.module('grafana.services');
+  /** @ngInject */
+  function CloudWatchDatasource(instanceSettings, $q, backendSrv, templateSrv) {
+    this.type = 'cloudwatch';
+    this.name = instanceSettings.name;
+    this.supportMetrics = true;
+    this.proxyUrl = instanceSettings.url;
+    this.defaultRegion = instanceSettings.jsonData.defaultRegion;
 
-  module.factory('CloudWatchDatasource', function($q, backendSrv, templateSrv) {
-
-    function CloudWatchDatasource(datasource) {
-      this.type = 'cloudwatch';
-      this.name = datasource.name;
-      this.supportMetrics = true;
-      this.proxyUrl = datasource.url;
-      this.defaultRegion = datasource.jsonData.defaultRegion;
-    }
-
-    CloudWatchDatasource.prototype.query = function(options) {
+    this.query = function(options) {
       var start = convertToCloudWatchTime(options.range.from, false);
       var end = convertToCloudWatchTime(options.range.to, true);
 
@@ -72,7 +67,7 @@ function (angular, _, moment, dateMath) {
       });
     };
 
-    CloudWatchDatasource.prototype.performTimeSeriesQuery = function(query, start, end) {
+    this.performTimeSeriesQuery = function(query, start, end) {
       return this.awsRequest({
         region: query.region,
         action: 'GetMetricStatistics',
@@ -88,15 +83,15 @@ function (angular, _, moment, dateMath) {
       });
     };
 
-    CloudWatchDatasource.prototype.getRegions = function() {
+    this.getRegions = function() {
       return this.awsRequest({action: '__GetRegions'});
     };
 
-    CloudWatchDatasource.prototype.getNamespaces = function() {
+    this.getNamespaces = function() {
       return this.awsRequest({action: '__GetNamespaces'});
     };
 
-    CloudWatchDatasource.prototype.getMetrics = function(namespace) {
+    this.getMetrics = function(namespace) {
       return this.awsRequest({
         action: '__GetMetrics',
         parameters: {
@@ -105,7 +100,7 @@ function (angular, _, moment, dateMath) {
       });
     };
 
-    CloudWatchDatasource.prototype.getDimensionKeys = function(namespace) {
+    this.getDimensionKeys = function(namespace) {
       return this.awsRequest({
         action: '__GetDimensions',
         parameters: {
@@ -114,7 +109,7 @@ function (angular, _, moment, dateMath) {
       });
     };
 
-    CloudWatchDatasource.prototype.getDimensionValues = function(region, namespace, metricName, dimensionKey, filterDimensions) {
+    this.getDimensionValues = function(region, namespace, metricName, dimensionKey, filterDimensions) {
       var request = {
         region: templateSrv.replace(region),
         action: 'ListMetrics',
@@ -141,7 +136,7 @@ function (angular, _, moment, dateMath) {
       });
     };
 
-    CloudWatchDatasource.prototype.performEC2DescribeInstances = function(region, filters, instanceIds) {
+    this.performEC2DescribeInstances = function(region, filters, instanceIds) {
       return this.awsRequest({
         region: region,
         action: 'DescribeInstances',
@@ -149,7 +144,7 @@ function (angular, _, moment, dateMath) {
       });
     };
 
-    CloudWatchDatasource.prototype.metricFindQuery = function(query) {
+    this.metricFindQuery = function(query) {
       var region;
       var namespace;
       var metricName;
@@ -210,7 +205,7 @@ function (angular, _, moment, dateMath) {
       return $q.when([]);
     };
 
-    CloudWatchDatasource.prototype.performDescribeAlarmsForMetric = function(region, namespace, metricName, dimensions, statistic, period) {
+    this.performDescribeAlarmsForMetric = function(region, namespace, metricName, dimensions, statistic, period) {
       return this.awsRequest({
         region: region,
         action: 'DescribeAlarmsForMetric',
@@ -218,7 +213,7 @@ function (angular, _, moment, dateMath) {
       });
     };
 
-    CloudWatchDatasource.prototype.performDescribeAlarmHistory = function(region, alarmName, startDate, endDate) {
+    this.performDescribeAlarmHistory = function(region, alarmName, startDate, endDate) {
       return this.awsRequest({
         region: region,
         action: 'DescribeAlarmHistory',
@@ -226,7 +221,7 @@ function (angular, _, moment, dateMath) {
       });
     };
 
-    CloudWatchDatasource.prototype.annotationQuery = function(options) {
+    this.annotationQuery = function(options) {
       var annotation = options.annotation;
       var region = templateSrv.replace(annotation.region);
       var namespace = templateSrv.replace(annotation.namespace);
@@ -278,7 +273,7 @@ function (angular, _, moment, dateMath) {
       return d.promise;
     };
 
-    CloudWatchDatasource.prototype.testDatasource = function() {
+    this.testDatasource = function() {
       /* use billing metrics for test */
       var region = this.defaultRegion;
       var namespace = 'AWS/Billing';
@@ -290,7 +285,7 @@ function (angular, _, moment, dateMath) {
       });
     };
 
-    CloudWatchDatasource.prototype.awsRequest = function(data) {
+    this.awsRequest = function(data) {
       var options = {
         method: 'POST',
         url: this.proxyUrl,
@@ -302,7 +297,7 @@ function (angular, _, moment, dateMath) {
       });
     };
 
-    CloudWatchDatasource.prototype.getDefaultRegion = function() {
+    this.getDefaultRegion = function() {
       return this.defaultRegion;
     };
 
@@ -361,7 +356,7 @@ function (angular, _, moment, dateMath) {
       });
     }
 
-    return CloudWatchDatasource;
-  });
+  }
 
+  return CloudWatchDatasource;
 });

+ 9 - 1
public/app/plugins/datasource/cloudwatch/directives.js → public/app/plugins/datasource/cloudwatch/module.js

@@ -1,8 +1,9 @@
 define([
   'angular',
+  './datasource',
   './query_parameter_ctrl',
 ],
-function (angular) {
+function (angular, CloudWatchDatasource) {
   'use strict';
 
   var module = angular.module('grafana.directives');
@@ -28,4 +29,11 @@ function (angular) {
     };
   });
 
+  module.directive('datasourceCustomSettingsViewCloudwatch', function() {
+    return {templateUrl: 'app/plugins/datasource/cloudwatch/partials/edit_view.html'};
+  });
+
+  return  {
+    Datasource: CloudWatchDatasource
+  };
 });

+ 0 - 0
public/app/plugins/datasource/cloudwatch/partials/config.html → public/app/plugins/datasource/cloudwatch/partials/edit_view.html


+ 2 - 11
public/app/plugins/datasource/cloudwatch/plugin.json

@@ -1,16 +1,7 @@
 {
-  "pluginType": "datasource",
+  "type": "datasource",
   "name": "CloudWatch",
-
-  "type": "cloudwatch",
-  "serviceName": "CloudWatchDatasource",
-
-  "module": "app/plugins/datasource/cloudwatch/datasource",
-
-  "partials": {
-    "config": "app/plugins/datasource/cloudwatch/partials/config.html",
-    "query": "app/plugins/datasource/cloudwatch/partials/query.editor.html"
-  },
+  "id": "cloudwatch",
 
   "metrics": true,
   "annotations": true

+ 10 - 10
public/app/plugins/datasource/cloudwatch/specs/datasource_specs.ts

@@ -3,25 +3,25 @@ import "../datasource";
 import {describe, beforeEach, it, sinon, expect, angularMocks} from 'test/lib/common';
 import moment from 'moment';
 import helpers from 'test/specs/helpers';
+import Datasource from "../datasource";
 
 describe('CloudWatchDatasource', function() {
   var ctx = new helpers.ServiceTestContext();
+  var instanceSettings = {
+    jsonData: {defaultRegion: 'us-east-1', access: 'proxy'},
+  };
 
   beforeEach(angularMocks.module('grafana.core'));
   beforeEach(angularMocks.module('grafana.services'));
   beforeEach(angularMocks.module('grafana.controllers'));
-
   beforeEach(ctx.providePhase(['templateSrv', 'backendSrv']));
-  beforeEach(ctx.createService('CloudWatchDatasource'));
 
-  beforeEach(function() {
-    ctx.ds = new ctx.service({
-      jsonData: {
-        defaultRegion: 'us-east-1',
-        access: 'proxy'
-      }
-    });
-  });
+  beforeEach(angularMocks.inject(function($q, $rootScope, $httpBackend, $injector) {
+    ctx.$q = $q;
+    ctx.$httpBackend =  $httpBackend;
+    ctx.$rootScope = $rootScope;
+    ctx.ds = $injector.instantiate(Datasource, {instanceSettings: instanceSettings});
+  }));
 
   describe('When performing CloudWatch query', function() {
     var requestParams;

+ 3 - 0
public/app/plugins/datasource/elasticsearch/datasource.d.ts

@@ -0,0 +1,3 @@
+declare var Datasource: any;
+export default Datasource;
+

+ 36 - 42
public/app/plugins/datasource/elasticsearch/datasource.js

@@ -7,33 +7,27 @@ define([
   './index_pattern',
   './elastic_response',
   './query_ctrl',
-  './directives'
 ],
 function (angular, _, moment, kbn, ElasticQueryBuilder, IndexPattern, ElasticResponse) {
   'use strict';
 
-  var module = angular.module('grafana.services');
-
-  module.factory('ElasticDatasource', function($q, backendSrv, templateSrv, timeSrv) {
-
-    function ElasticDatasource(datasource) {
-      this.type = 'elasticsearch';
-      this.basicAuth = datasource.basicAuth;
-      this.withCredentials = datasource.withCredentials;
-      this.url = datasource.url;
-      this.name = datasource.name;
-      this.index = datasource.index;
-      this.timeField = datasource.jsonData.timeField;
-      this.esVersion = datasource.jsonData.esVersion;
-      this.indexPattern = new IndexPattern(datasource.index, datasource.jsonData.interval);
-      this.interval = datasource.jsonData.timeInterval;
-      this.queryBuilder = new ElasticQueryBuilder({
-        timeField: this.timeField,
-        esVersion: this.esVersion,
-      });
-    }
-
-    ElasticDatasource.prototype._request = function(method, url, data) {
+  /** @ngInject */
+  function ElasticDatasource(instanceSettings, $q, backendSrv, templateSrv, timeSrv) {
+    this.basicAuth = instanceSettings.basicAuth;
+    this.withCredentials = instanceSettings.withCredentials;
+    this.url = instanceSettings.url;
+    this.name = instanceSettings.name;
+    this.index = instanceSettings.index;
+    this.timeField = instanceSettings.jsonData.timeField;
+    this.esVersion = instanceSettings.jsonData.esVersion;
+    this.indexPattern = new IndexPattern(instanceSettings.index, instanceSettings.jsonData.interval);
+    this.interval = instanceSettings.jsonData.timeInterval;
+    this.queryBuilder = new ElasticQueryBuilder({
+      timeField: this.timeField,
+      esVersion: this.esVersion,
+    });
+
+    this._request = function(method, url, data) {
       var options = {
         url: this.url + "/" + url,
         method: method,
@@ -52,21 +46,21 @@ function (angular, _, moment, kbn, ElasticQueryBuilder, IndexPattern, ElasticRes
       return backendSrv.datasourceRequest(options);
     };
 
-    ElasticDatasource.prototype._get = function(url) {
+    this._get = function(url) {
       return this._request('GET', this.indexPattern.getIndexForToday() + url)
-        .then(function(results) {
-          return results.data;
-        });
+      .then(function(results) {
+        return results.data;
+      });
     };
 
-    ElasticDatasource.prototype._post = function(url, data) {
+    this._post = function(url, data) {
       return this._request('POST', url, data)
-        .then(function(results) {
-          return results.data;
-        });
+      .then(function(results) {
+        return results.data;
+      });
     };
 
-    ElasticDatasource.prototype.annotationQuery = function(options) {
+    this.annotationQuery = function(options) {
       var annotation = options.annotation;
       var timeField = annotation.timeField || '@timestamp';
       var queryString = annotation.query || '*';
@@ -147,7 +141,7 @@ function (angular, _, moment, kbn, ElasticQueryBuilder, IndexPattern, ElasticRes
       });
     };
 
-    ElasticDatasource.prototype.testDatasource = function() {
+    this.testDatasource = function() {
       return this._get('/_stats').then(function() {
         return { status: "success", message: "Data source is working", title: "Success" };
       }, function(err) {
@@ -159,13 +153,13 @@ function (angular, _, moment, kbn, ElasticQueryBuilder, IndexPattern, ElasticRes
       });
     };
 
-    ElasticDatasource.prototype.getQueryHeader = function(searchType, timeFrom, timeTo) {
+    this.getQueryHeader = function(searchType, timeFrom, timeTo) {
       var header = {search_type: searchType, "ignore_unavailable": true};
       header.index = this.indexPattern.getIndexList(timeFrom, timeTo);
       return angular.toJson(header);
     };
 
-    ElasticDatasource.prototype.query = function(options) {
+    this.query = function(options) {
       var payload = "";
       var target;
       var sentTargets = [];
@@ -203,7 +197,7 @@ function (angular, _, moment, kbn, ElasticQueryBuilder, IndexPattern, ElasticRes
       });
     };
 
-    ElasticDatasource.prototype.getFields = function(query) {
+    this.getFields = function(query) {
       return this._get('/_mapping').then(function(res) {
         var fields = {};
         var typeMap = {
@@ -240,7 +234,7 @@ function (angular, _, moment, kbn, ElasticQueryBuilder, IndexPattern, ElasticRes
       });
     };
 
-    ElasticDatasource.prototype.getTerms = function(queryDef) {
+    this.getTerms = function(queryDef) {
       var range = timeSrv.timeRange();
       var header = this.getQueryHeader('count', range.from, range.to);
       var esQuery = angular.toJson(this.queryBuilder.getTermsQuery(queryDef));
@@ -258,7 +252,7 @@ function (angular, _, moment, kbn, ElasticQueryBuilder, IndexPattern, ElasticRes
       });
     };
 
-    ElasticDatasource.prototype.metricFindQuery = function(query) {
+    this.metricFindQuery = function(query) {
       query = templateSrv.replace(query);
       query = angular.fromJson(query);
       if (!query) {
@@ -273,14 +267,14 @@ function (angular, _, moment, kbn, ElasticQueryBuilder, IndexPattern, ElasticRes
       }
     };
 
-    ElasticDatasource.prototype.getDashboard = function(id) {
+    this.getDashboard = function(id) {
       return this._get('/dashboard/' + id)
       .then(function(result) {
         return angular.fromJson(result._source.dashboard);
       });
     };
 
-    ElasticDatasource.prototype.searchDashboards = function() {
+    this.searchDashboards = function() {
       var query = {
         query: { query_string: { query: '*' } },
         size: 10000,
@@ -308,7 +302,7 @@ function (angular, _, moment, kbn, ElasticQueryBuilder, IndexPattern, ElasticRes
         return displayHits;
       });
     };
+  }
 
-    return ElasticDatasource;
-  });
+  return ElasticDatasource;
 });

+ 4 - 0
public/app/plugins/datasource/elasticsearch/directives.js

@@ -20,6 +20,10 @@ function (angular) {
     return {templateUrl: 'app/plugins/datasource/elasticsearch/partials/annotations.editor.html'};
   });
 
+  module.directive('elastic', function() {
+    return {templateUrl: 'app/plugins/datasource/elasticsearch/partials/config.html'};
+  });
+
   module.directive('elasticMetricAgg', function() {
     return {
       templateUrl: 'app/plugins/datasource/elasticsearch/partials/metric_agg.html',

+ 38 - 0
public/app/plugins/datasource/elasticsearch/edit_view.ts

@@ -0,0 +1,38 @@
+///<reference path="../../../headers/common.d.ts" />
+
+import angular from 'angular';
+import _ from 'lodash';
+
+export class EditViewCtrl {
+
+  constructor($scope) {
+    $scope.indexPatternTypes = [
+      {name: 'No pattern',  value: undefined},
+      {name: 'Hourly',      value: 'Hourly',  example: '[logstash-]YYYY.MM.DD.HH'},
+      {name: 'Daily',       value: 'Daily',   example: '[logstash-]YYYY.MM.DD'},
+      {name: 'Weekly',      value: 'Weekly',  example: '[logstash-]GGGG.WW'},
+      {name: 'Monthly',     value: 'Monthly', example: '[logstash-]YYYY.MM'},
+      {name: 'Yearly',      value: 'Yearly',  example: '[logstash-]YYYY'},
+    ];
+
+    $scope.esVersions = [
+      {name: '1.x', value: 1},
+      {name: '2.x', value: 2},
+    ];
+
+    $scope.indexPatternTypeChanged = function() {
+      var def = _.findWhere($scope.indexPatternTypes, {value: $scope.current.jsonData.interval});
+      $scope.current.database = def.example || 'es-index-name';
+    };
+  }
+}
+
+function editViewDirective() {
+  return {
+    templateUrl: 'app/plugins/datasource/elasticsearch/partials/edit_view.html',
+    controller: EditViewCtrl,
+  };
+};
+
+
+export default editViewDirective;

+ 60 - 0
public/app/plugins/datasource/elasticsearch/module.js

@@ -0,0 +1,60 @@
+define([
+  'angular',
+  './datasource',
+  './edit_view',
+  './bucket_agg',
+  './metric_agg',
+],
+function (angular, ElasticDatasource, editView) {
+  'use strict';
+
+  var module = angular.module('grafana.directives');
+
+  module.directive('metricQueryEditorElasticsearch', function() {
+    return {controller: 'ElasticQueryCtrl', templateUrl: 'app/plugins/datasource/elasticsearch/partials/query.editor.html'};
+  });
+
+  module.directive('metricQueryOptionsElasticsearch', function() {
+    return {templateUrl: 'app/plugins/datasource/elasticsearch/partials/query.options.html'};
+  });
+
+  module.directive('annotationsQueryEditorElasticsearch', function() {
+    return {templateUrl: 'app/plugins/datasource/elasticsearch/partials/annotations.editor.html'};
+  });
+
+  module.directive('elasticMetricAgg', function() {
+    return {
+      templateUrl: 'app/plugins/datasource/elasticsearch/partials/metric_agg.html',
+      controller: 'ElasticMetricAggCtrl',
+      restrict: 'E',
+      scope: {
+        target: "=",
+        index: "=",
+        onChange: "&",
+        getFields: "&",
+        esVersion: '='
+      }
+    };
+  });
+
+  module.directive('elasticBucketAgg', function() {
+    return {
+      templateUrl: 'app/plugins/datasource/elasticsearch/partials/bucket_agg.html',
+      controller: 'ElasticBucketAggCtrl',
+      restrict: 'E',
+      scope: {
+        target: "=",
+        index: "=",
+        onChange: "&",
+        getFields: "&",
+      }
+    };
+  });
+
+  module.directive('datasourceCustomSettingsViewElasticsearch', editView.default);
+
+  return {
+    Datasource: ElasticDatasource,
+  };
+
+});

+ 3 - 4
public/app/plugins/datasource/elasticsearch/partials/config.html → public/app/plugins/datasource/elasticsearch/partials/edit_view.html

@@ -1,5 +1,4 @@
-<div ng-include="httpConfigPartialSrc"></div>
-<br>
+<datasource-http-settings></datasource-http-settings>
 
 <h5>Elasticsearch details</h5>
 
@@ -42,8 +41,8 @@
 	</ul>
 	<div class="clearfix"></div>
 </div>
-</div>
 
+<br>
 <h5>Default query settings</h5>
 
 <div class="tight-form last">
@@ -53,7 +52,7 @@
 		</li>
 		<li>
 			<input type="text" class="input-medium tight-form-input input-xlarge" ng-model="current.jsonData.timeInterval"
-						 spellcheck='false' placeholder="example: >10s">
+			spellcheck='false' placeholder="example: >10s">
 		</li>
 		<li class="tight-form-item">
 			<i class="fa fa-question-circle" bs-tooltip="'Set a low limit by having a greater sign: example: >10s'" data-placement="right"></i>

+ 2 - 11
public/app/plugins/datasource/elasticsearch/plugin.json

@@ -1,16 +1,7 @@
 {
-  "pluginType": "datasource",
+  "type": "datasource",
   "name": "Elasticsearch",
-
-  "type": "elasticsearch",
-  "serviceName": "ElasticDatasource",
-
-  "module": "app/plugins/datasource/elasticsearch/datasource",
-
-  "partials": {
-    "config": "app/plugins/datasource/elasticsearch/partials/config.html",
-    "annotations": "app/plugins/datasource/elasticsearch/partials/annotations.editor.html"
-  },
+  "id": "elasticsearch",
 
   "defaultMatchFormat": "lucene",
   "annotations": true,

+ 16 - 16
public/app/plugins/datasource/elasticsearch/specs/datasource_specs.ts

@@ -1,28 +1,32 @@
 
-import "../datasource";
 import {describe, beforeEach, it, sinon, expect, angularMocks} from 'test/lib/common';
 import moment from 'moment';
 import angular from 'angular';
 import helpers from 'test/specs/helpers';
+import Datasource from "../datasource";
 
 describe('ElasticDatasource', function() {
   var ctx = new helpers.ServiceTestContext();
+  var instanceSettings: any = {jsonData: {}};
 
   beforeEach(angularMocks.module('grafana.core'));
   beforeEach(angularMocks.module('grafana.services'));
   beforeEach(ctx.providePhase(['templateSrv', 'backendSrv']));
-  beforeEach(ctx.createService('ElasticDatasource'));
-  beforeEach(function() {
-    ctx.ds = new ctx.service({jsonData: {}});
-  });
+  beforeEach(angularMocks.inject(function($q, $rootScope, $httpBackend, $injector) {
+    ctx.$q = $q;
+    ctx.$httpBackend =  $httpBackend;
+    ctx.$rootScope = $rootScope;
+    ctx.$injector = $injector;
+  }));
+
+  function createDatasource(instanceSettings) {
+    instanceSettings.jsonData = instanceSettings.jsonData || {};
+    ctx.ds = ctx.$injector.instantiate(Datasource, {instanceSettings: instanceSettings});
+  }
 
   describe('When testing datasource with index pattern', function() {
     beforeEach(function() {
-      ctx.ds = new ctx.service({
-        url: 'http://es.com',
-        index: '[asd-]YYYY.MM.DD',
-        jsonData: { interval: 'Daily' }
-      });
+      createDatasource({url: 'http://es.com', index: '[asd-]YYYY.MM.DD', jsonData: {interval: 'Daily'}});
     });
 
     it('should translate index pattern to current day', function() {
@@ -44,11 +48,7 @@ describe('ElasticDatasource', function() {
     var requestOptions, parts, header;
 
     beforeEach(function() {
-      ctx.ds = new ctx.service({
-        url: 'http://es.com',
-        index: '[asd-]YYYY.MM.DD',
-        jsonData: { interval: 'Daily' }
-      });
+      createDatasource({url: 'http://es.com', index: '[asd-]YYYY.MM.DD', jsonData: {interval: 'Daily'}});
 
       ctx.backendSrv.datasourceRequest = function(options) {
         requestOptions = options;
@@ -83,7 +83,7 @@ describe('ElasticDatasource', function() {
     var requestOptions, parts, header;
 
     beforeEach(function() {
-      ctx.ds = new ctx.service({url: 'http://es.com', index: 'test', jsonData: {}});
+      createDatasource({url: 'http://es.com', index: 'test'});
 
       ctx.backendSrv.datasourceRequest = function(options) {
         requestOptions = options;

+ 0 - 30
public/app/plugins/datasource/grafana/datasource.js

@@ -1,30 +0,0 @@
-define([
-  'angular'
-],
-function (angular) {
-  'use strict';
-
-  var module = angular.module('grafana.services');
-
-  module.factory('GrafanaDatasource', function($q, backendSrv) {
-
-    function GrafanaDatasource() {
-    }
-
-    GrafanaDatasource.prototype.query = function(options) {
-      return backendSrv.get('/api/metrics/test', {
-        from: options.range.from.valueOf(),
-        to: options.range.to.valueOf(),
-        maxDataPoints: options.maxDataPoints
-      });
-    };
-
-    GrafanaDatasource.prototype.metricFindQuery = function() {
-      return $q.when([]);
-    };
-
-    return GrafanaDatasource;
-
-  });
-
-});

+ 0 - 13
public/app/plugins/datasource/grafana/directives.js

@@ -1,13 +0,0 @@
-define([
-  'angular',
-],
-function (angular) {
-  'use strict';
-
-  var module = angular.module('grafana.directives');
-
-  module.directive('metricQueryEditorGrafana', function() {
-    return {templateUrl: 'app/plugins/datasource/grafana/partials/query.editor.html'};
-  });
-
-});

+ 3 - 6
public/app/plugins/datasource/grafana/plugin.json

@@ -1,11 +1,8 @@
 {
-  "pluginType": "datasource",
+  "type": "datasource",
   "name": "Grafana",
-  "builtIn": true,
-
-  "type": "grafana",
-  "serviceName": "GrafanaDatasource",
+  "id": "grafana",
 
-  "module": "app/plugins/datasource/grafana/datasource",
+  "builtIn": true,
   "metrics": true
 }

+ 3 - 0
public/app/plugins/datasource/graphite/datasource.d.ts

@@ -0,0 +1,3 @@
+declare var Datasource: any;
+export default Datasource;
+

+ 55 - 63
public/app/plugins/datasource/graphite/datasource.js

@@ -4,7 +4,6 @@ define([
   'jquery',
   'app/core/config',
   'app/core/utils/datemath',
-  './directives',
   './query_ctrl',
   './func_editor',
   './add_graphite_func',
@@ -12,20 +11,16 @@ define([
 function (angular, _, $, config, dateMath) {
   'use strict';
 
-  var module = angular.module('grafana.services');
+  /** @ngInject */
+  function GraphiteDatasource(instanceSettings, $q, backendSrv, templateSrv) {
+    this.basicAuth = instanceSettings.basicAuth;
+    this.url = instanceSettings.url;
+    this.name = instanceSettings.name;
+    this.cacheTimeout = instanceSettings.cacheTimeout;
+    this.withCredentials = instanceSettings.withCredentials;
+    this.render_method = instanceSettings.render_method || 'POST';
 
-  module.factory('GraphiteDatasource', function($q, backendSrv, templateSrv) {
-
-    function GraphiteDatasource(datasource) {
-      this.basicAuth = datasource.basicAuth;
-      this.url = datasource.url;
-      this.name = datasource.name;
-      this.cacheTimeout = datasource.cacheTimeout;
-      this.withCredentials = datasource.withCredentials;
-      this.render_method = datasource.render_method || 'POST';
-    }
-
-    GraphiteDatasource.prototype.query = function(options) {
+    this.query = function(options) {
       try {
         var graphOptions = {
           from: this.translateTime(options.rangeRaw.from, false),
@@ -62,7 +57,7 @@ function (angular, _, $, config, dateMath) {
       }
     };
 
-    GraphiteDatasource.prototype.convertDataPointsToMs = function(result) {
+    this.convertDataPointsToMs = function(result) {
       if (!result || !result.data) { return []; }
       for (var i = 0; i < result.data.length; i++) {
         var series = result.data[i];
@@ -73,7 +68,7 @@ function (angular, _, $, config, dateMath) {
       return result;
     };
 
-    GraphiteDatasource.prototype.annotationQuery = function(options) {
+    this.annotationQuery = function(options) {
       // Graphite metric as annotation
       if (options.annotation.target) {
         var target = templateSrv.replace(options.annotation.target);
@@ -85,50 +80,49 @@ function (angular, _, $, config, dateMath) {
         };
 
         return this.query(graphiteQuery)
-          .then(function(result) {
-            var list = [];
-
-            for (var i = 0; i < result.data.length; i++) {
-              var target = result.data[i];
-
-              for (var y = 0; y < target.datapoints.length; y++) {
-                var datapoint = target.datapoints[y];
-                if (!datapoint[0]) { continue; }
-
-                list.push({
-                  annotation: options.annotation,
-                  time: datapoint[1],
-                  title: target.target
-                });
-              }
-            }
+        .then(function(result) {
+          var list = [];
 
-            return list;
-          });
-      }
-      // Graphite event as annotation
-      else {
-        var tags = templateSrv.replace(options.annotation.tags);
-        return this.events({range: options.rangeRaw, tags: tags})
-          .then(function(results) {
-            var list = [];
-            for (var i = 0; i < results.data.length; i++) {
-              var e = results.data[i];
+          for (var i = 0; i < result.data.length; i++) {
+            var target = result.data[i];
+
+            for (var y = 0; y < target.datapoints.length; y++) {
+              var datapoint = target.datapoints[y];
+              if (!datapoint[0]) { continue; }
 
               list.push({
                 annotation: options.annotation,
-                time: e.when * 1000,
-                title: e.what,
-                tags: e.tags,
-                text: e.data
+                time: datapoint[1],
+                title: target.target
               });
             }
-            return list;
-          });
+          }
+
+          return list;
+        });
+      }
+      // Graphite event as annotation
+      else {
+        var tags = templateSrv.replace(options.annotation.tags);
+        return this.events({range: options.rangeRaw, tags: tags}).then(function(results) {
+          var list = [];
+          for (var i = 0; i < results.data.length; i++) {
+            var e = results.data[i];
+
+            list.push({
+              annotation: options.annotation,
+              time: e.when * 1000,
+              title: e.what,
+              tags: e.tags,
+              text: e.data
+            });
+          }
+          return list;
+        });
       }
     };
 
-    GraphiteDatasource.prototype.events = function(options) {
+    this.events = function(options) {
       try {
         var tags = '';
         if (options.tags) {
@@ -146,7 +140,7 @@ function (angular, _, $, config, dateMath) {
       }
     };
 
-    GraphiteDatasource.prototype.translateTime = function(date, roundUp) {
+    this.translateTime = function(date, roundUp) {
       if (_.isString(date)) {
         if (date === 'now') {
           return 'now';
@@ -178,7 +172,7 @@ function (angular, _, $, config, dateMath) {
       return date.unix();
     };
 
-    GraphiteDatasource.prototype.metricFindQuery = function(query) {
+    this.metricFindQuery = function(query) {
       var interpolated;
       try {
         interpolated = encodeURIComponent(templateSrv.replace(query));
@@ -198,24 +192,24 @@ function (angular, _, $, config, dateMath) {
       });
     };
 
-    GraphiteDatasource.prototype.testDatasource = function() {
+    this.testDatasource = function() {
       return this.metricFindQuery('*').then(function () {
         return { status: "success", message: "Data source is working", title: "Success" };
       });
     };
 
-    GraphiteDatasource.prototype.listDashboards = function(query) {
+    this.listDashboards = function(query) {
       return this.doGraphiteRequest({ method: 'GET',  url: '/dashboard/find/', params: {query: query || ''} })
       .then(function(results) {
         return results.data.dashboards;
       });
     };
 
-    GraphiteDatasource.prototype.loadDashboard = function(dashName) {
+    this.loadDashboard = function(dashName) {
       return this.doGraphiteRequest({method: 'GET', url: '/dashboard/load/' + encodeURIComponent(dashName) });
     };
 
-    GraphiteDatasource.prototype.doGraphiteRequest = function(options) {
+    this.doGraphiteRequest = function(options) {
       if (this.basicAuth || this.withCredentials) {
         options.withCredentials = true;
       }
@@ -230,9 +224,9 @@ function (angular, _, $, config, dateMath) {
       return backendSrv.datasourceRequest(options);
     };
 
-    GraphiteDatasource.prototype._seriesRefLetters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
+    this._seriesRefLetters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
 
-    GraphiteDatasource.prototype.buildGraphiteParams = function(options, scopedVars) {
+    this.buildGraphiteParams = function(options, scopedVars) {
       var graphite_options = ['from', 'until', 'rawData', 'format', 'maxDataPoints', 'cacheTimeout'];
       var clean_options = [], targets = {};
       var target, targetValue, i;
@@ -296,9 +290,7 @@ function (angular, _, $, config, dateMath) {
 
       return clean_options;
     };
+  }
 
-    return GraphiteDatasource;
-
-  });
-
+  return GraphiteDatasource;
 });

+ 9 - 1
public/app/plugins/datasource/graphite/directives.js → public/app/plugins/datasource/graphite/module.js

@@ -1,7 +1,8 @@
 define([
   'angular',
+  './datasource',
 ],
-function (angular) {
+function (angular, GraphiteDatasource) {
   'use strict';
 
   var module = angular.module('grafana.directives');
@@ -18,4 +19,11 @@ function (angular) {
     return {templateUrl: 'app/plugins/datasource/graphite/partials/annotations.editor.html'};
   });
 
+  module.directive('datasourceCustomSettingsViewGraphite', function() {
+    return {templateUrl: 'app/plugins/datasource/graphite/partials/config.html'};
+  });
+
+  return {
+    Datasource: GraphiteDatasource,
+  };
 });

+ 1 - 2
public/app/plugins/datasource/graphite/partials/config.html

@@ -1,3 +1,2 @@
-<div ng-include="httpConfigPartialSrc"></div>
-
+<datasource-http-settings></datasource-http-settings>
 

+ 2 - 10
public/app/plugins/datasource/graphite/plugin.json

@@ -1,15 +1,7 @@
 {
-  "pluginType": "datasource",
   "name": "Graphite",
-
-  "type": "graphite",
-  "serviceName": "GraphiteDatasource",
-
-  "module": "app/plugins/datasource/graphite/datasource",
-
-  "partials": {
-    "config": "app/plugins/datasource/graphite/partials/config.html"
-  },
+  "type": "datasource",
+  "id": "graphite",
 
   "defaultMatchFormat": "glob",
   "metrics": true,

+ 9 - 4
public/app/plugins/datasource/graphite/specs/datasource_specs.ts

@@ -1,19 +1,24 @@
 
-import "../datasource";
 import {describe, beforeEach, it, sinon, expect, angularMocks} from 'test/lib/common';
 import helpers from 'test/specs/helpers';
+import Datasource from "../datasource";
 
 describe('graphiteDatasource', function() {
   var ctx = new helpers.ServiceTestContext();
+  var instanceSettings: any = {url: ['']};
 
   beforeEach(angularMocks.module('grafana.core'));
   beforeEach(angularMocks.module('grafana.services'));
-
   beforeEach(ctx.providePhase(['backendSrv']));
-  beforeEach(ctx.createService('GraphiteDatasource'));
+  beforeEach(angularMocks.inject(function($q, $rootScope, $httpBackend, $injector) {
+    ctx.$q = $q;
+    ctx.$httpBackend =  $httpBackend;
+    ctx.$rootScope = $rootScope;
+    ctx.$injector = $injector;
+  }));
 
   beforeEach(function() {
-    ctx.ds = new ctx.service({ url: [''] });
+    ctx.ds = ctx.$injector.instantiate(Datasource, {instanceSettings: instanceSettings});
   });
 
   describe('When querying influxdb with one target using query editor target spec', function() {

+ 20 - 27
public/app/plugins/datasource/influxdb/datasource.js

@@ -4,7 +4,6 @@ define([
   'app/core/utils/datemath',
   './influx_series',
   './influx_query',
-  './directives',
   './query_ctrl',
 ],
 function (angular, _, dateMath, InfluxSeries, InfluxQuery) {
@@ -12,27 +11,22 @@ function (angular, _, dateMath, InfluxSeries, InfluxQuery) {
 
   InfluxQuery = InfluxQuery.default;
 
-  var module = angular.module('grafana.services');
+  function InfluxDatasource(instanceSettings, $q, backendSrv, templateSrv) {
+    this.type = 'influxdb';
+    this.urls = _.map(instanceSettings.url.split(','), function(url) {
+      return url.trim();
+    });
 
-  module.factory('InfluxDatasource', function($q, backendSrv, templateSrv) {
+    this.username = instanceSettings.username;
+    this.password = instanceSettings.password;
+    this.name = instanceSettings.name;
+    this.database = instanceSettings.database;
+    this.basicAuth = instanceSettings.basicAuth;
 
-    function InfluxDatasource(datasource) {
-      this.type = 'influxdb';
-      this.urls = _.map(datasource.url.split(','), function(url) {
-        return url.trim();
-      });
-
-      this.username = datasource.username;
-      this.password = datasource.password;
-      this.name = datasource.name;
-      this.database = datasource.database;
-      this.basicAuth = datasource.basicAuth;
+    this.supportAnnotations = true;
+    this.supportMetrics = true;
 
-      this.supportAnnotations = true;
-      this.supportMetrics = true;
-    }
-
-    InfluxDatasource.prototype.query = function(options) {
+    this.query = function(options) {
       var timeFilter = getTimeFilter(options);
       var queryTargets = [];
       var i, y;
@@ -93,7 +87,7 @@ function (angular, _, dateMath, InfluxSeries, InfluxQuery) {
       });
     };
 
-    InfluxDatasource.prototype.annotationQuery = function(options) {
+    this.annotationQuery = function(options) {
       var timeFilter = getTimeFilter({rangeRaw: options.rangeRaw});
       var query = options.annotation.query.replace('$timeFilter', timeFilter);
       query = templateSrv.replace(query);
@@ -106,7 +100,7 @@ function (angular, _, dateMath, InfluxSeries, InfluxQuery) {
       });
     };
 
-    InfluxDatasource.prototype.metricFindQuery = function (query) {
+    this.metricFindQuery = function (query) {
       var interpolated;
       try {
         interpolated = templateSrv.replace(query);
@@ -133,17 +127,17 @@ function (angular, _, dateMath, InfluxSeries, InfluxQuery) {
       });
     };
 
-    InfluxDatasource.prototype._seriesQuery = function(query) {
+    this._seriesQuery = function(query) {
       return this._influxRequest('GET', '/query', {q: query, epoch: 'ms'});
     };
 
-    InfluxDatasource.prototype.testDatasource = function() {
+    this.testDatasource = function() {
       return this.metricFindQuery('SHOW MEASUREMENTS LIMIT 1').then(function () {
         return { status: "success", message: "Data source is working", title: "Success" };
       });
     };
 
-    InfluxDatasource.prototype._influxRequest = function(method, url, data) {
+    this._influxRequest = function(method, url, data) {
       var self = this;
 
       var currentUrl = self.urls.shift();
@@ -219,9 +213,8 @@ function (angular, _, dateMath, InfluxSeries, InfluxQuery) {
       }
       return (date.valueOf() / 1000).toFixed(0) + 's';
     }
+  }
 
-    return InfluxDatasource;
-
-  });
+  return InfluxDatasource;
 
 });

+ 9 - 1
public/app/plugins/datasource/influxdb/directives.js → public/app/plugins/datasource/influxdb/module.js

@@ -1,7 +1,8 @@
 define([
   'angular',
+  './datasource',
 ],
-function (angular) {
+function (angular, InfluxDatasource) {
   'use strict';
 
   var module = angular.module('grafana.directives');
@@ -18,4 +19,11 @@ function (angular) {
     return {templateUrl: 'app/plugins/datasource/influxdb/partials/annotations.editor.html'};
   });
 
+  module.directive('datasourceCustomSettingsViewInfluxdb', function() {
+    return {templateUrl: 'app/plugins/datasource/influxdb/partials/config.html'};
+  });
+
+  return {
+    Datasource: InfluxDatasource
+  };
 });

+ 1 - 2
public/app/plugins/datasource/influxdb/partials/config.html

@@ -1,5 +1,4 @@
-<div ng-include="httpConfigPartialSrc"></div>
-<br>
+<datasource-http-settings></datasource-http-settings>
 
 <h5>InfluxDB Details</h5>
 

+ 2 - 10
public/app/plugins/datasource/influxdb/plugin.json

@@ -1,15 +1,7 @@
 {
-  "pluginType": "datasource",
+  "type": "datasource",
   "name": "InfluxDB 0.9.x",
-
-  "type": "influxdb",
-  "serviceName": "InfluxDatasource",
-
-  "module": "app/plugins/datasource/influxdb/datasource",
-
-  "partials": {
-    "config": "app/plugins/datasource/influxdb/partials/config.html"
-  },
+  "id": "influxdb",
 
   "defaultMatchFormat": "regex values",
   "metrics": true,

+ 0 - 35
public/app/plugins/datasource/mixed/datasource.js

@@ -1,35 +0,0 @@
-define([
-  'angular',
-  'lodash',
-],
-function (angular, _) {
-  'use strict';
-
-  var module = angular.module('grafana.services');
-
-  module.factory('MixedDatasource', function($q, backendSrv, datasourceSrv) {
-
-    function MixedDatasource() {
-    }
-
-    MixedDatasource.prototype.query = function(options) {
-      var sets = _.groupBy(options.targets, 'datasource');
-      var promises = _.map(sets, function(targets) {
-        return datasourceSrv.get(targets[0].datasource).then(function(ds) {
-          var opt = angular.copy(options);
-          opt.targets = targets;
-          return ds.query(opt);
-        });
-      });
-
-      return $q.all(promises).then(function(results) {
-        return { data: _.flatten(_.pluck(results, 'data')) };
-      });
-
-    };
-
-    return MixedDatasource;
-
-  });
-
-});

+ 32 - 0
public/app/plugins/datasource/mixed/datasource.ts

@@ -0,0 +1,32 @@
+///<reference path="../../../headers/common.d.ts" />
+
+import angular from 'angular';
+import _ from 'lodash';
+
+class MixedDatasource {
+
+  constructor(private $q, private datasourceSrv) {
+  }
+
+  query(options) {
+    var sets = _.groupBy(options.targets, 'datasource');
+    var promises = _.map(sets, targets => {
+      var dsName = targets[0].datasource;
+      if (dsName === '-- Mixed --') {
+        return this.$q([]);
+      }
+
+      return this.datasourceSrv.get(dsName).then(function(ds) {
+        var opt = angular.copy(options);
+        opt.targets = targets;
+        return ds.query(opt);
+      });
+    });
+
+    return this.$q.all(promises).then(function(results) {
+      return { data: _.flatten(_.pluck(results, 'data')) };
+    });
+  }
+}
+
+export {MixedDatasource, MixedDatasource as Datasource}

+ 3 - 6
public/app/plugins/datasource/mixed/plugin.json

@@ -1,12 +1,9 @@
 {
-  "pluginType": "datasource",
+  "type": "datasource",
   "name": "Mixed datasource",
+  "id": "mixed",
+
   "builtIn": true,
   "mixed": true,
-
-  "type": "mixed",
-  "serviceName": "MixedDatasource",
-
-  "module": "app/plugins/datasource/mixed/datasource",
   "metrics": true
 }

+ 3 - 0
public/app/plugins/datasource/opentsdb/datasource.d.ts

@@ -0,0 +1,3 @@
+declare var Datasource: any;
+export default Datasource;
+

+ 16 - 22
public/app/plugins/datasource/opentsdb/datasource.js

@@ -3,25 +3,19 @@ define([
   'lodash',
   'app/core/utils/datemath',
   'moment',
-  './directives',
   './queryCtrl',
 ],
 function (angular, _, dateMath) {
   'use strict';
 
-  var module = angular.module('grafana.services');
-
-  module.factory('OpenTSDBDatasource', function($q, backendSrv, templateSrv) {
-
-    function OpenTSDBDatasource(datasource) {
-      this.type = 'opentsdb';
-      this.url = datasource.url;
-      this.name = datasource.name;
-      this.supportMetrics = true;
-    }
+  function OpenTSDBDatasource(instanceSettings, $q, backendSrv, templateSrv) {
+    this.type = 'opentsdb';
+    this.url = instanceSettings.url;
+    this.name = instanceSettings.name;
+    this.supportMetrics = true;
 
     // Called once per panel (graph)
-    OpenTSDBDatasource.prototype.query = function(options) {
+    this.query = function(options) {
       var start = convertToTSDBTime(options.rangeRaw.from, false);
       var end = convertToTSDBTime(options.rangeRaw.to, true);
       var qs = [];
@@ -60,7 +54,7 @@ function (angular, _, dateMath) {
       });
     };
 
-    OpenTSDBDatasource.prototype.performTimeSeriesQuery = function(queries, start, end) {
+    this.performTimeSeriesQuery = function(queries, start, end) {
       var reqBody = {
         start: start,
         queries: queries
@@ -80,13 +74,13 @@ function (angular, _, dateMath) {
       return backendSrv.datasourceRequest(options);
     };
 
-    OpenTSDBDatasource.prototype._performSuggestQuery = function(query, type) {
+    this._performSuggestQuery = function(query, type) {
       return this._get('/api/suggest', {type: type, q: query, max: 1000}).then(function(result) {
         return result.data;
       });
     };
 
-    OpenTSDBDatasource.prototype._performMetricKeyValueLookup = function(metric, key) {
+    this._performMetricKeyValueLookup = function(metric, key) {
       if(!metric || !key) {
         return $q.when([]);
       }
@@ -105,7 +99,7 @@ function (angular, _, dateMath) {
       });
     };
 
-    OpenTSDBDatasource.prototype._performMetricKeyLookup = function(metric) {
+    this._performMetricKeyLookup = function(metric) {
       if(!metric) { return $q.when([]); }
 
       return this._get('/api/search/lookup', {m: metric, limit: 1000}).then(function(result) {
@@ -122,7 +116,7 @@ function (angular, _, dateMath) {
       });
     };
 
-    OpenTSDBDatasource.prototype._get = function(relativeUrl, params) {
+    this._get = function(relativeUrl, params) {
       return backendSrv.datasourceRequest({
         method: 'GET',
         url: this.url + relativeUrl,
@@ -130,7 +124,7 @@ function (angular, _, dateMath) {
       });
     };
 
-    OpenTSDBDatasource.prototype.metricFindQuery = function(query) {
+    this.metricFindQuery = function(query) {
       if (!query) { return $q.when([]); }
 
       var interpolated;
@@ -181,14 +175,14 @@ function (angular, _, dateMath) {
       return $q.when([]);
     };
 
-    OpenTSDBDatasource.prototype.testDatasource = function() {
+    this.testDatasource = function() {
       return this._performSuggestQuery('cpu', 'metrics').then(function () {
         return { status: "success", message: "Data source is working", title: "Success" };
       });
     };
 
     var aggregatorsPromise = null;
-    OpenTSDBDatasource.prototype.getAggregators = function() {
+    this.getAggregators = function() {
       if (aggregatorsPromise) { return aggregatorsPromise; }
 
       aggregatorsPromise =  this._get('/api/aggregators').then(function(result) {
@@ -311,7 +305,7 @@ function (angular, _, dateMath) {
       return date.valueOf();
     }
 
-    return OpenTSDBDatasource;
-  });
+  }
 
+  return OpenTSDBDatasource;
 });

+ 9 - 1
public/app/plugins/datasource/opentsdb/directives.js → public/app/plugins/datasource/opentsdb/module.js

@@ -1,7 +1,8 @@
 define([
   'angular',
+  './datasource',
 ],
-function (angular) {
+function (angular, OpenTsDatasource) {
   'use strict';
 
   var module = angular.module('grafana.directives');
@@ -13,4 +14,11 @@ function (angular) {
     };
   });
 
+  module.directive('datasourceCustomSettingsViewOpentsdb', function() {
+    return {templateUrl: 'app/plugins/datasource/opentsdb/partials/config.html'};
+  });
+
+  return {
+    Datasource: OpenTsDatasource
+  };
 });

+ 1 - 3
public/app/plugins/datasource/opentsdb/partials/config.html

@@ -1,4 +1,2 @@
-<div ng-include="httpConfigPartialSrc"></div>
-
-<br>
+<datasource-http-settings></datasource-http-settings>
 

+ 2 - 10
public/app/plugins/datasource/opentsdb/plugin.json

@@ -1,15 +1,7 @@
 {
-  "pluginType": "datasource",
+  "type": "datasource",
   "name": "OpenTSDB",
-
-  "type": "opentsdb",
-  "serviceName": "OpenTSDBDatasource",
-
-  "module": "app/plugins/datasource/opentsdb/datasource",
-
-  "partials": {
-    "config": "app/plugins/datasource/opentsdb/partials/config.html"
-  },
+  "id": "opentsdb",
 
   "metrics": true,
   "defaultMatchFormat": "pipe"

+ 71 - 0
public/app/plugins/datasource/opentsdb/specs/datasource-specs.ts

@@ -0,0 +1,71 @@
+import {describe, beforeEach, it, sinon, expect, angularMocks} from 'test/lib/common';
+import helpers from 'test/specs/helpers';
+import Datasource from "../datasource";
+
+describe('opentsdb', function() {
+  var ctx = new helpers.ServiceTestContext();
+  var instanceSettings = {url: '' };
+
+  beforeEach(angularMocks.module('grafana.core'));
+  beforeEach(angularMocks.module('grafana.services'));
+  beforeEach(ctx.providePhase(['backendSrv']));
+
+  beforeEach(angularMocks.inject(function($q, $rootScope, $httpBackend, $injector) {
+    ctx.$q = $q;
+    ctx.$httpBackend =  $httpBackend;
+    ctx.$rootScope = $rootScope;
+    ctx.ds = $injector.instantiate(Datasource, {instanceSettings: instanceSettings});
+  }));
+
+  describe('When performing metricFindQuery', function() {
+    var results;
+    var requestOptions;
+
+    beforeEach(function() {
+      ctx.backendSrv.datasourceRequest = function(options) {
+        requestOptions = options;
+        return ctx.$q.when({data: [{ target: 'prod1.count', datapoints: [[10, 1], [12,1]] }]});
+      };
+    });
+
+    it('metrics() should generate api suggest query', function() {
+      ctx.ds.metricFindQuery('metrics(pew)').then(function(data) { results = data; });
+      ctx.$rootScope.$apply();
+      expect(requestOptions.url).to.be('/api/suggest');
+      expect(requestOptions.params.type).to.be('metrics');
+      expect(requestOptions.params.q).to.be('pew');
+    });
+
+    it('tag_names(cpu) should generate looku  query', function() {
+      ctx.ds.metricFindQuery('tag_names(cpu)').then(function(data) { results = data; });
+      ctx.$rootScope.$apply();
+      expect(requestOptions.url).to.be('/api/search/lookup');
+      expect(requestOptions.params.m).to.be('cpu');
+    });
+
+    it('tag_values(cpu, test) should generate looku  query', function() {
+      ctx.ds.metricFindQuery('tag_values(cpu, hostname)').then(function(data) { results = data; });
+      ctx.$rootScope.$apply();
+      expect(requestOptions.url).to.be('/api/search/lookup');
+      expect(requestOptions.params.m).to.be('cpu{hostname=*}');
+    });
+
+    it('suggest_tagk() should generate api suggest query', function() {
+      ctx.ds.metricFindQuery('suggest_tagk(foo)').then(function(data) { results = data; });
+      ctx.$rootScope.$apply();
+      expect(requestOptions.url).to.be('/api/suggest');
+      expect(requestOptions.params.type).to.be('tagk');
+      expect(requestOptions.params.q).to.be('foo');
+    });
+
+    it('suggest_tagv() should generate api suggest query', function() {
+      ctx.ds.metricFindQuery('suggest_tagv(bar)').then(function(data) { results = data; });
+      ctx.$rootScope.$apply();
+      expect(requestOptions.url).to.be('/api/suggest');
+      expect(requestOptions.params.type).to.be('tagv');
+      expect(requestOptions.params.q).to.be('bar');
+    });
+  });
+
+});
+

+ 3 - 0
public/app/plugins/datasource/prometheus/datasource.d.ts

@@ -0,0 +1,3 @@
+declare var Datasource: any;
+export default Datasource;
+

+ 34 - 41
public/app/plugins/datasource/prometheus/datasource.js

@@ -3,31 +3,25 @@ define([
   'lodash',
   'moment',
   'app/core/utils/datemath',
-  './directives',
   './query_ctrl',
 ],
 function (angular, _, moment, dateMath) {
   'use strict';
 
-  var module = angular.module('grafana.services');
-
   var durationSplitRegexp = /(\d+)(ms|s|m|h|d|w|M|y)/;
 
-  module.factory('PrometheusDatasource', function($q, backendSrv, templateSrv) {
-
-    function PrometheusDatasource(datasource) {
-      this.type = 'prometheus';
-      this.editorSrc = 'app/features/prometheus/partials/query.editor.html';
-      this.name = datasource.name;
-      this.supportMetrics = true;
-      this.url = datasource.url;
-      this.directUrl = datasource.directUrl;
-      this.basicAuth = datasource.basicAuth;
-      this.withCredentials = datasource.withCredentials;
-      this.lastErrors = {};
-    }
-
-    PrometheusDatasource.prototype._request = function(method, url) {
+  function PrometheusDatasource(instanceSettings, $q, backendSrv, templateSrv) {
+    this.type = 'prometheus';
+    this.editorSrc = 'app/features/prometheus/partials/query.editor.html';
+    this.name = instanceSettings.name;
+    this.supportMetrics = true;
+    this.url = instanceSettings.url;
+    this.directUrl = instanceSettings.directUrl;
+    this.basicAuth = instanceSettings.basicAuth;
+    this.withCredentials = instanceSettings.withCredentials;
+    this.lastErrors = {};
+
+    this._request = function(method, url) {
       var options = {
         url: this.url + url,
         method: method
@@ -46,7 +40,7 @@ function (angular, _, moment, dateMath) {
     };
 
     // Called once per panel (graph)
-    PrometheusDatasource.prototype.query = function(options) {
+    this.query = function(options) {
       var start = getPrometheusTime(options.range.from, false);
       var end = getPrometheusTime(options.range.to, true);
 
@@ -86,31 +80,31 @@ function (angular, _, moment, dateMath) {
 
       var self = this;
       return $q.all(allQueryPromise)
-        .then(function(allResponse) {
-          var result = [];
-
-          _.each(allResponse, function(response, index) {
-            if (response.status === 'error') {
-              self.lastErrors.query = response.error;
-              throw response.error;
-            }
-            delete self.lastErrors.query;
-
-            _.each(response.data.data.result, function(metricData) {
-              result.push(transformMetricData(metricData, options.targets[index]));
-            });
+      .then(function(allResponse) {
+        var result = [];
+
+        _.each(allResponse, function(response, index) {
+          if (response.status === 'error') {
+            self.lastErrors.query = response.error;
+            throw response.error;
+          }
+          delete self.lastErrors.query;
+
+          _.each(response.data.data.result, function(metricData) {
+            result.push(transformMetricData(metricData, options.targets[index]));
           });
-
-          return { data: result };
         });
+
+        return { data: result };
+      });
     };
 
-    PrometheusDatasource.prototype.performTimeSeriesQuery = function(query, start, end) {
+    this.performTimeSeriesQuery = function(query, start, end) {
       var url = '/api/v1/query_range?query=' + encodeURIComponent(query.expr) + '&start=' + start + '&end=' + end + '&step=' + query.step;
       return this._request('GET', url);
     };
 
-    PrometheusDatasource.prototype.performSuggestQuery = function(query) {
+    this.performSuggestQuery = function(query) {
       var url = '/api/v1/label/__name__/values';
 
       return this._request('GET', url).then(function(result) {
@@ -120,7 +114,7 @@ function (angular, _, moment, dateMath) {
       });
     };
 
-    PrometheusDatasource.prototype.metricFindQuery = function(query) {
+    this.metricFindQuery = function(query) {
       if (!query) { return $q.when([]); }
 
       var interpolated;
@@ -196,7 +190,7 @@ function (angular, _, moment, dateMath) {
       }
     };
 
-    PrometheusDatasource.prototype.testDatasource = function() {
+    this.testDatasource = function() {
       return this.metricFindQuery('metrics(.*)').then(function() {
         return { status: 'success', message: 'Data source is working', title: 'Success' };
       });
@@ -276,8 +270,7 @@ function (angular, _, moment, dateMath) {
       }
       return (date.valueOf() / 1000).toFixed(0);
     }
+  }
 
-    return PrometheusDatasource;
-  });
-
+  return PrometheusDatasource;
 });

+ 9 - 1
public/app/plugins/datasource/prometheus/directives.js → public/app/plugins/datasource/prometheus/module.js

@@ -1,7 +1,8 @@
 define([
   'angular',
+  './datasource',
 ],
-function (angular) {
+function (angular, PromDatasource) {
   'use strict';
 
   var module = angular.module('grafana.directives');
@@ -10,4 +11,11 @@ function (angular) {
     return {controller: 'PrometheusQueryCtrl', templateUrl: 'app/plugins/datasource/prometheus/partials/query.editor.html'};
   });
 
+  module.directive('datasourceCustomSettingsViewPrometheus', function() {
+    return {templateUrl: 'app/plugins/datasource/prometheus/partials/config.html'};
+  });
+
+  return {
+    Datasource: PromDatasource
+  };
 });

+ 1 - 3
public/app/plugins/datasource/prometheus/partials/config.html

@@ -1,4 +1,2 @@
-<div ng-include="httpConfigPartialSrc"></div>
-
-<br>
+<datasource-http-settings></datasource-http-settings>
 

+ 2 - 10
public/app/plugins/datasource/prometheus/plugin.json

@@ -1,15 +1,7 @@
 {
-  "pluginType": "datasource",
+  "type": "datasource",
   "name": "Prometheus",
-
-  "type": "prometheus",
-  "serviceName": "PrometheusDatasource",
-
-  "module": "app/plugins/datasource/prometheus/datasource",
-
-  "partials": {
-    "config": "app/plugins/datasource/prometheus/partials/config.html"
-  },
+  "id": "prometheus",
 
   "metrics": true
 }

+ 9 - 6
public/app/plugins/datasource/prometheus/specs/datasource_specs.ts

@@ -1,17 +1,20 @@
-import '../datasource';
 import {describe, beforeEach, it, sinon, expect, angularMocks} from 'test/lib/common';
 import moment from 'moment';
 import helpers from 'test/specs/helpers';
+import Datasource from '../datasource';
 
 describe('PrometheusDatasource', function() {
-
   var ctx = new helpers.ServiceTestContext();
+  var instanceSettings = {url: 'proxied', directUrl: 'direct', user: 'test', password: 'mupp' };
+
   beforeEach(angularMocks.module('grafana.core'));
   beforeEach(angularMocks.module('grafana.services'));
-  beforeEach(ctx.createService('PrometheusDatasource'));
-  beforeEach(function() {
-    ctx.ds = new ctx.service({ url: 'proxied', directUrl: 'direct', user: 'test', password: 'mupp' });
-  });
+  beforeEach(angularMocks.inject(function($q, $rootScope, $httpBackend, $injector) {
+    ctx.$q = $q;
+    ctx.$httpBackend =  $httpBackend;
+    ctx.$rootScope = $rootScope;
+    ctx.ds = $injector.instantiate(Datasource, {instanceSettings: instanceSettings});
+  }));
 
   describe('When querying prometheus with one target using query editor target spec', function() {
     var results;

+ 0 - 18
public/app/plugins/datasource/sql/datasource.js

@@ -1,18 +0,0 @@
-define([
-  'angular',
-],
-function (angular) {
-  'use strict';
-
-  var module = angular.module('grafana.services');
-
-  module.factory('SqlDatasource', function() {
-
-    function SqlDatasource() {
-    }
-
-    return SqlDatasource;
-
-  });
-
-});

+ 0 - 53
public/app/plugins/datasource/sql/partials/config.html

@@ -1,53 +0,0 @@
-<h2>SQL Options</h2>
-
-<div class="tight-form">
-	<ul class="tight-form-list">
-		<li class="tight-form-item" style="width: 80px">
-			DB Type
-		</li>
-		<li>
-			<select class="input-medium tight-form-input" ng-model="current.jsonData.dbType" ng-options="f for f in ['sqlite3','mysql','postgres']"></select>
-		</li>
-		<li class="tight-form-item" style="width: 80px">
-			Host
-		</li>
-		<li>
-			<input type="text" class="tight-form-input input-medium" ng-model='current.jsonData.host' placeholder="localhost:3306">
-		</li>
-		<li class="tight-form-item" ng-if="current.jsonData.dbType === 'postgres'">
-			SSL&nbsp;
-			<input class="cr1" id="jsonData.ssl" type="checkbox" ng-model="current.jsonData.ssl" ng-checked="current.jsonData.ssl">
-			<label for="jsonData.ssl" class="cr1"></label>
-		</li>
-	</ul>
-	<div class="clearfix"></div>
-</div>
-<div class="tight-form">
-	<ul class="tight-form-list">
-		<li class="tight-form-item" style="width: 80px">
-			Database
-		</li>
-		<li>
-			<input type="text" class="tight-form-input input-medium" ng-model='current.database' placeholder="">
-		</li>
-	</ul>
-	<div class="clearfix"></div>
-</div>
-<div class="tight-form">
-	<ul class="tight-form-list">
-		<li class="tight-form-item" style="width: 80px">
-			User
-		</li>
-		<li>
-			<input type="text" class="tight-form-input input-medium" ng-model='current.user' placeholder="">
-		</li>
-		<li class="tight-form-item" style="width: 80px">
-			Password
-		</li>
-		<li>
-			<input type="password" class="tight-form-input input-medium" ng-model='current.password' placeholder="">
-		</li>
-	</ul>
-	<div class="clearfix"></div>
-</div>
-

+ 0 - 17
public/app/plugins/datasource/sql/partials/query.editor.html

@@ -1,17 +0,0 @@
-
-<div class="fluid-row" style="margin-top: 20px">
-	<div class="span2"></div>
-	<div class="grafana-info-box span8">
-		<h5>Test graph</h5>
-
-		<p>
-		This is just a test data source that generates random walk series. If this is your only data source
-		open the left side menu and navigate to the data sources admin screen and add your data sources. You can change
-		data source using the button to the left of the <strong>Add query</strong> button.
-		</p>
-	</div>
-	<div class="span2"></div>
-
-	<div class="clearfix"></div>
-</div>
-

部分文件因为文件数量过多而无法显示