Jelajahi Sumber

Merge branch 'external-plugins'

Torkel Ödegaard 10 tahun lalu
induk
melakukan
2ec5bc77d7
85 mengubah file dengan 1123 tambahan dan 143 penghapusan
  1. 15 4
      pkg/api/api.go
  2. 9 2
      pkg/api/datasources.go
  3. 25 0
      pkg/api/dtos/index.go
  4. 8 0
      pkg/api/dtos/plugin_bundle.go
  5. 75 0
      pkg/api/externalplugin.go
  6. 17 1
      pkg/api/frontendsettings.go
  7. 88 33
      pkg/api/index.go
  8. 7 7
      pkg/api/login.go
  9. 65 0
      pkg/api/plugin_bundle.go
  10. 15 8
      pkg/cmd/web.go
  11. 1 1
      pkg/login/ldap.go
  12. 34 0
      pkg/models/plugin_bundle.go
  13. 85 0
      pkg/plugins/models.go
  14. 149 17
      pkg/plugins/plugins.go
  15. 5 2
      pkg/plugins/plugins_test.go
  16. 1 0
      pkg/services/sqlstore/migrations/migrations.go
  17. 26 0
      pkg/services/sqlstore/migrations/plugin_bundle.go
  18. 46 0
      pkg/services/sqlstore/plugin_bundle.go
  19. 2 4
      pkg/setting/setting.go
  20. 11 6
      public/app/app.js
  21. 1 0
      public/app/core/config.js
  22. 5 11
      public/app/core/controllers/sidemenu_ctrl.js
  23. 10 0
      public/app/core/routes/all.js
  24. 2 2
      public/app/core/services/context_srv.js
  25. 1 8
      public/app/core/settings.js
  26. 4 0
      public/app/features/org/all.js
  27. 3 0
      public/app/features/org/partials/pluginConfigCore.html
  28. 42 0
      public/app/features/org/partials/pluginEdit.html
  29. 41 0
      public/app/features/org/partials/plugins.html
  30. 35 0
      public/app/features/org/pluginEditCtrl.js
  31. 47 0
      public/app/features/org/plugin_directive.js
  32. 58 0
      public/app/features/org/plugin_srv.js
  33. 33 0
      public/app/features/org/pluginsCtrl.js
  34. 2 2
      public/app/features/panel/panel_directive.js
  35. 13 0
      public/app/plugins/external/example/readme.md
  36. 0 0
      public/app/plugins/panels/dashlist/editor.html
  37. 0 0
      public/app/plugins/panels/dashlist/module.html
  38. 2 2
      public/app/plugins/panels/dashlist/module.js
  39. 8 0
      public/app/plugins/panels/dashlist/plugin.json
  40. 0 0
      public/app/plugins/panels/graph/axisEditor.html
  41. 0 0
      public/app/plugins/panels/graph/graph.js
  42. 0 0
      public/app/plugins/panels/graph/graph.tooltip.js
  43. 1 1
      public/app/plugins/panels/graph/legend.js
  44. 0 0
      public/app/plugins/panels/graph/legend.popover.html
  45. 0 0
      public/app/plugins/panels/graph/module.html
  46. 3 3
      public/app/plugins/panels/graph/module.js
  47. 8 0
      public/app/plugins/panels/graph/plugin.json
  48. 0 0
      public/app/plugins/panels/graph/seriesOverridesCtrl.js
  49. 0 0
      public/app/plugins/panels/graph/styleEditor.html
  50. 0 0
      public/app/plugins/panels/singlestat/editor.html
  51. 0 0
      public/app/plugins/panels/singlestat/module.html
  52. 2 2
      public/app/plugins/panels/singlestat/module.js
  53. 8 0
      public/app/plugins/panels/singlestat/plugin.json
  54. 0 0
      public/app/plugins/panels/singlestat/singleStatPanel.js
  55. 2 2
      public/app/plugins/panels/table/controller.ts
  56. 0 0
      public/app/plugins/panels/table/editor.html
  57. 1 3
      public/app/plugins/panels/table/editor.ts
  58. 0 0
      public/app/plugins/panels/table/module.html
  59. 2 2
      public/app/plugins/panels/table/module.ts
  60. 0 0
      public/app/plugins/panels/table/options.html
  61. 8 0
      public/app/plugins/panels/table/plugin.json
  62. 1 1
      public/app/plugins/panels/table/renderer.ts
  63. 0 0
      public/app/plugins/panels/table/specs/renderer_specs.ts
  64. 0 0
      public/app/plugins/panels/table/specs/table_model_specs.ts
  65. 0 0
      public/app/plugins/panels/table/specs/transformers_specs.ts
  66. 52 0
      public/app/plugins/panels/table/table_model.ts
  67. 1 1
      public/app/plugins/panels/table/transformers.ts
  68. 0 0
      public/app/plugins/panels/text/editor.html
  69. 0 0
      public/app/plugins/panels/text/module.html
  70. 3 3
      public/app/plugins/panels/text/module.js
  71. 8 0
      public/app/plugins/panels/text/plugin.json
  72. 9 0
      public/app/plugins/plugin.json
  73. 0 0
      public/app/plugins/plugin_api.md
  74. 1 1
      public/test/specs/graph-ctrl-specs.js
  75. 1 1
      public/test/specs/graph-specs.js
  76. 1 1
      public/test/specs/graph-tooltip-specs.js
  77. 1 1
      public/test/specs/seriesOverridesCtrl-specs.js
  78. 1 1
      public/test/specs/singlestat-specs.js
  79. 2 0
      public/test/test-main.js
  80. 0 0
      public/vendor/showdown.js
  81. 11 1
      public/views/index.html
  82. 1 3
      tasks/options/htmlmin.js
  83. 1 2
      tasks/options/jscs.js
  84. 1 2
      tasks/options/jshint.js
  85. 2 2
      tasks/options/requirejs.js

+ 15 - 4
pkg/api/api.go

@@ -13,7 +13,7 @@ func Register(r *macaron.Macaron) {
 	reqSignedIn := middleware.Auth(&middleware.AuthOptions{ReqSignedIn: true})
 	reqGrafanaAdmin := middleware.Auth(&middleware.AuthOptions{ReqSignedIn: true, ReqGrafanaAdmin: true})
 	reqEditorRole := middleware.RoleAuth(m.ROLE_EDITOR, m.ROLE_ADMIN)
-	regOrgAdmin := middleware.RoleAuth(m.ROLE_ADMIN)
+	reqOrgAdmin := middleware.RoleAuth(m.ROLE_ADMIN)
 	quota := middleware.Quota
 	bind := binding.Bind
 
@@ -41,6 +41,9 @@ 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("/dashboard/*", reqSignedIn, Index)
 	r.Get("/dashboard-solo/*", reqSignedIn, Index)
 
@@ -114,7 +117,7 @@ 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))
-		}, regOrgAdmin)
+		}, reqOrgAdmin)
 
 		// create new org
 		r.Post("/orgs", quota("org"), bind(m.CreateOrgCommand{}), wrap(CreateOrg))
@@ -141,7 +144,7 @@ func Register(r *macaron.Macaron) {
 			r.Get("/", wrap(GetApiKeys))
 			r.Post("/", quota("api_key"), bind(m.AddApiKeyCommand{}), wrap(AddApiKey))
 			r.Delete("/:id", wrap(DeleteApiKey))
-		}, regOrgAdmin)
+		}, reqOrgAdmin)
 
 		// Data sources
 		r.Group("/datasources", func() {
@@ -151,7 +154,13 @@ func Register(r *macaron.Macaron) {
 			r.Delete("/:id", DeleteDataSource)
 			r.Get("/:id", wrap(GetDataSourceById))
 			r.Get("/plugins", GetDataSourcePlugins)
-		}, regOrgAdmin)
+		}, reqOrgAdmin)
+
+		// PluginBundles
+		r.Group("/plugins", func() {
+			r.Get("/", wrap(GetPluginBundles))
+			r.Post("/", bind(m.UpdatePluginBundleCmd{}), wrap(UpdatePluginBundle))
+		}, reqOrgAdmin)
 
 		r.Get("/frontend/settings/", GetFrontendSettings)
 		r.Any("/datasources/proxy/:id/*", reqSignedIn, ProxyDataSourceRequest)
@@ -188,5 +197,7 @@ func Register(r *macaron.Macaron) {
 	// rendering
 	r.Get("/render/*", reqSignedIn, RenderToPng)
 
+	InitExternalPluginRoutes(r)
+
 	r.NotFound(NotFoundHandler)
 }

+ 9 - 2
pkg/api/datasources.go

@@ -117,8 +117,15 @@ func UpdateDataSource(c *middleware.Context, cmd m.UpdateDataSourceCommand) {
 func GetDataSourcePlugins(c *middleware.Context) {
 	dsList := make(map[string]interface{})
 
-	for key, value := range plugins.DataSources {
-		if value.(map[string]interface{})["builtIn"] == nil {
+	orgBundles := m.GetPluginBundlesQuery{OrgId: c.OrgId}
+	err := bus.Dispatch(&orgBundles)
+	if err != nil {
+		c.JsonApiErr(500, "Failed to get org plugin Bundles", err)
+	}
+	enabledPlugins := plugins.GetEnabledPlugins(orgBundles.Result)
+
+	for key, value := range enabledPlugins.DataSourcePlugins {
+		if !value.BuiltIn {
 			dsList[key] = value
 		}
 	}

+ 25 - 0
pkg/api/dtos/index.go

@@ -0,0 +1,25 @@
+package dtos
+
+type IndexViewData struct {
+	User               *CurrentUser
+	Settings           map[string]interface{}
+	AppUrl             string
+	AppSubUrl          string
+	GoogleAnalyticsId  string
+	GoogleTagManagerId string
+
+	PluginCss    []*PluginCss
+	PluginJs     []string
+	MainNavLinks []*NavLink
+}
+
+type PluginCss struct {
+	Light string `json:"light"`
+	Dark  string `json:"dark"`
+}
+
+type NavLink struct {
+	Text string `json:"text"`
+	Icon string `json:"icon"`
+	Href string `json:"href"`
+}

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

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

+ 75 - 0
pkg/api/externalplugin.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 InitExternalPluginRoutes(r *macaron.Macaron) {
+	for _, plugin := range plugins.ExternalPlugins {
+		log.Info("Plugin: Adding proxy routes for backend 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, ExternalPlugin(route.Url))
+			r.Route(url, route.Method, handlers...)
+			log.Info("Plugin: Adding route %s", url)
+		}
+	}
+}
+
+func ExternalPlugin(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 := NewExternalPluginProxy(string(ctx), path, targetUrl)
+		proxy.Transport = dataProxyTransport
+		proxy.ServeHTTP(c.RW(), c.Req.Request)
+	}
+}
+
+func NewExternalPluginProxy(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}
+}

+ 17 - 1
pkg/api/frontendsettings.go

@@ -29,6 +29,13 @@ func getFrontendSettingsMap(c *middleware.Context) (map[string]interface{}, erro
 	datasources := make(map[string]interface{})
 	var defaultDatasource string
 
+	orgBundles := m.GetPluginBundlesQuery{OrgId: c.OrgId}
+	err := bus.Dispatch(&orgBundles)
+	if err != nil {
+		return nil, err
+	}
+	enabledPlugins := plugins.GetEnabledPlugins(orgBundles.Result)
+
 	for _, ds := range orgDataSources {
 		url := ds.Url
 
@@ -42,7 +49,7 @@ func getFrontendSettingsMap(c *middleware.Context) (map[string]interface{}, erro
 			"url":  url,
 		}
 
-		meta, exists := plugins.DataSources[ds.Type]
+		meta, exists := enabledPlugins.DataSourcePlugins[ds.Type]
 		if !exists {
 			log.Error(3, "Could not find plugin definition for data source: %v", ds.Type)
 			continue
@@ -109,9 +116,18 @@ func getFrontendSettingsMap(c *middleware.Context) (map[string]interface{}, erro
 		defaultDatasource = "-- Grafana --"
 	}
 
+	panels := map[string]interface{}{}
+	for _, panel := range enabledPlugins.PanelPlugins {
+		panels[panel.Type] = map[string]interface{}{
+			"module": panel.Module,
+			"name":   panel.Name,
+		}
+	}
+
 	jsonObj := map[string]interface{}{
 		"defaultDatasource": defaultDatasource,
 		"datasources":       datasources,
+		"panels":            panels,
 		"appSubUrl":         setting.AppSubUrl,
 		"allowOrgCreate":    (setting.AllowUserOrgCreate && c.IsSignedIn) || c.IsGrafanaAdmin,
 		"authProxyEnabled":  setting.AuthProxyEnabled,

+ 88 - 33
pkg/api/index.go

@@ -2,66 +2,121 @@ 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"
 	"github.com/grafana/grafana/pkg/setting"
 )
 
-func setIndexViewData(c *middleware.Context) error {
+func setIndexViewData(c *middleware.Context) (*dtos.IndexViewData, error) {
 	settings, err := getFrontendSettingsMap(c)
 	if err != nil {
-		return err
+		return nil, err
 	}
 
-	currentUser := &dtos.CurrentUser{
-		Id:             c.UserId,
-		IsSignedIn:     c.IsSignedIn,
-		Login:          c.Login,
-		Email:          c.Email,
-		Name:           c.Name,
-		LightTheme:     c.Theme == "light",
-		OrgId:          c.OrgId,
-		OrgName:        c.OrgName,
-		OrgRole:        c.OrgRole,
-		GravatarUrl:    dtos.GetGravatarUrl(c.Email),
-		IsGrafanaAdmin: c.IsGrafanaAdmin,
+	var data = dtos.IndexViewData{
+		User: &dtos.CurrentUser{
+			Id:             c.UserId,
+			IsSignedIn:     c.IsSignedIn,
+			Login:          c.Login,
+			Email:          c.Email,
+			Name:           c.Name,
+			LightTheme:     c.Theme == "light",
+			OrgId:          c.OrgId,
+			OrgName:        c.OrgName,
+			OrgRole:        c.OrgRole,
+			GravatarUrl:    dtos.GetGravatarUrl(c.Email),
+			IsGrafanaAdmin: c.IsGrafanaAdmin,
+		},
+		Settings:           settings,
+		AppUrl:             setting.AppUrl,
+		AppSubUrl:          setting.AppSubUrl,
+		GoogleAnalyticsId:  setting.GoogleAnalyticsId,
+		GoogleTagManagerId: setting.GoogleTagManagerId,
 	}
 
 	if setting.DisableGravatar {
-		currentUser.GravatarUrl = setting.AppSubUrl + "/img/user_profile.png"
+		data.User.GravatarUrl = setting.AppSubUrl + "/img/user_profile.png"
 	}
 
-	if len(currentUser.Name) == 0 {
-		currentUser.Name = currentUser.Login
+	if len(data.User.Name) == 0 {
+		data.User.Name = data.User.Login
 	}
 
 	themeUrlParam := c.Query("theme")
 	if themeUrlParam == "light" {
-		currentUser.LightTheme = true
+		data.User.LightTheme = true
 	}
 
-	c.Data["User"] = currentUser
-	c.Data["Settings"] = settings
-	c.Data["AppUrl"] = setting.AppUrl
-	c.Data["AppSubUrl"] = setting.AppSubUrl
+	data.MainNavLinks = append(data.MainNavLinks, &dtos.NavLink{
+		Text: "Dashboards",
+		Icon: "fa fa-fw fa-th-large",
+		Href: "/",
+	})
 
-	if setting.GoogleAnalyticsId != "" {
-		c.Data["GoogleAnalyticsId"] = setting.GoogleAnalyticsId
+	if c.OrgRole == m.ROLE_ADMIN {
+		data.MainNavLinks = append(data.MainNavLinks, &dtos.NavLink{
+			Text: "Data Sources",
+			Icon: "fa fa-fw fa-database",
+			Href: "/datasources",
+		}, &dtos.NavLink{
+			Text: "Plugins",
+			Icon: "fa fa-fw fa-cubes",
+			Href: "/plugins",
+		})
 	}
 
-	if setting.GoogleTagManagerId != "" {
-		c.Data["GoogleTagManagerId"] = setting.GoogleTagManagerId
+	orgBundles := m.GetPluginBundlesQuery{OrgId: c.OrgId}
+	err = bus.Dispatch(&orgBundles)
+	if err != nil {
+		return nil, err
+	}
+	enabledPlugins := plugins.GetEnabledPlugins(orgBundles.Result)
+
+	for _, plugin := range enabledPlugins.ExternalPlugins {
+		for _, js := range plugin.Js {
+			data.PluginJs = append(data.PluginJs, js.Module)
+		}
+		for _, css := range plugin.Css {
+			data.PluginCss = append(data.PluginCss, &dtos.PluginCss{Light: css.Light, Dark: css.Dark})
+		}
+		for _, item := range plugin.MainNavLinks {
+			// only show menu items for the specified roles.
+			var validRoles []m.RoleType
+			if string(item.ReqRole) == "" || item.ReqRole == m.ROLE_VIEWER {
+				validRoles = []m.RoleType{m.ROLE_ADMIN, m.ROLE_EDITOR, m.ROLE_VIEWER}
+			} else if item.ReqRole == m.ROLE_EDITOR {
+				validRoles = []m.RoleType{m.ROLE_ADMIN, m.ROLE_EDITOR}
+			} else if item.ReqRole == m.ROLE_ADMIN {
+				validRoles = []m.RoleType{m.ROLE_ADMIN}
+			}
+			ok := true
+			if len(validRoles) > 0 {
+				ok = false
+				for _, role := range validRoles {
+					if role == c.OrgRole {
+						ok = true
+						break
+					}
+				}
+			}
+			if ok {
+				data.MainNavLinks = append(data.MainNavLinks, &dtos.NavLink{Text: item.Text, Href: item.Href, Icon: item.Icon})
+			}
+		}
 	}
 
-	return nil
+	return &data, nil
 }
 
 func Index(c *middleware.Context) {
-	if err := setIndexViewData(c); err != nil {
+	if data, err := setIndexViewData(c); err != nil {
 		c.Handle(500, "Failed to get settings", err)
 		return
+	} else {
+		c.HTML(200, "index", data)
 	}
-
-	c.HTML(200, "index")
 }
 
 func NotFoundHandler(c *middleware.Context) {
@@ -70,10 +125,10 @@ func NotFoundHandler(c *middleware.Context) {
 		return
 	}
 
-	if err := setIndexViewData(c); err != nil {
+	if data, err := setIndexViewData(c); err != nil {
 		c.Handle(500, "Failed to get settings", err)
 		return
+	} else {
+		c.HTML(404, "index", data)
 	}
-
-	c.HTML(404, "index")
 }

+ 7 - 7
pkg/api/login.go

@@ -19,19 +19,19 @@ const (
 )
 
 func LoginView(c *middleware.Context) {
-	if err := setIndexViewData(c); err != nil {
+	viewData, err := setIndexViewData(c)
+	if err != nil {
 		c.Handle(500, "Failed to get settings", err)
 		return
 	}
 
-	settings := c.Data["Settings"].(map[string]interface{})
-	settings["googleAuthEnabled"] = setting.OAuthService.Google
-	settings["githubAuthEnabled"] = setting.OAuthService.GitHub
-	settings["disableUserSignUp"] = !setting.AllowUserSignUp
-	settings["loginHint"] = setting.LoginHint
+	viewData.Settings["googleAuthEnabled"] = setting.OAuthService.Google
+	viewData.Settings["githubAuthEnabled"] = setting.OAuthService.GitHub
+	viewData.Settings["disableUserSignUp"] = !setting.AllowUserSignUp
+	viewData.Settings["loginHint"] = setting.LoginHint
 
 	if !tryLoginUsingRememberCookie(c) {
-		c.HTML(200, VIEW_INDEX)
+		c.HTML(200, VIEW_INDEX, viewData)
 		return
 	}
 

+ 65 - 0
pkg/api/plugin_bundle.go

@@ -0,0 +1,65 @@
+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 GetPluginBundles(c *middleware.Context) Response {
+	query := m.GetPluginBundlesQuery{OrgId: c.OrgId}
+
+	if err := bus.Dispatch(&query); err != nil {
+		return ApiError(500, "Failed to list Plugin Bundles", err)
+	}
+
+	installedBundlesMap := make(map[string]*dtos.PluginBundle)
+	for t, b := range plugins.Bundles {
+		installedBundlesMap[t] = &dtos.PluginBundle{
+			Type:     b.Type,
+			Enabled:  b.Enabled,
+			Module:   b.Module,
+			JsonData: make(map[string]interface{}),
+		}
+	}
+
+	seenBundles := make(map[string]bool)
+
+	result := make([]*dtos.PluginBundle, 0)
+	for _, b := range query.Result {
+		if def, ok := installedBundlesMap[b.Type]; ok {
+			result = append(result, &dtos.PluginBundle{
+				Type:     b.Type,
+				Enabled:  b.Enabled,
+				Module:   def.Module,
+				JsonData: b.JsonData,
+			})
+			seenBundles[b.Type] = true
+		}
+	}
+
+	for t, b := range installedBundlesMap {
+		if _, ok := seenBundles[t]; !ok {
+			result = append(result, b)
+		}
+	}
+
+	return Json(200, result)
+}
+
+func UpdatePluginBundle(c *middleware.Context, cmd m.UpdatePluginBundleCmd) Response {
+	cmd.OrgId = c.OrgId
+
+	if _, ok := plugins.Bundles[cmd.Type]; !ok {
+		return ApiError(404, "Bundle type not installed.", nil)
+	}
+
+	err := bus.Dispatch(&cmd)
+	if err != nil {
+		return ApiError(500, "Failed to update plugin bundle", err)
+	}
+
+	return ApiSuccess("Plugin updated")
+}

+ 15 - 8
pkg/cmd/web.go

@@ -14,6 +14,7 @@ import (
 	"github.com/grafana/grafana/pkg/api/static"
 	"github.com/grafana/grafana/pkg/log"
 	"github.com/grafana/grafana/pkg/middleware"
+	"github.com/grafana/grafana/pkg/plugins"
 	"github.com/grafana/grafana/pkg/setting"
 )
 
@@ -28,12 +29,18 @@ func newMacaron() *macaron.Macaron {
 		m.Use(middleware.Gziper())
 	}
 
-	mapStatic(m, "", "public")
-	mapStatic(m, "app", "app")
-	mapStatic(m, "css", "css")
-	mapStatic(m, "img", "img")
-	mapStatic(m, "fonts", "fonts")
-	mapStatic(m, "robots.txt", "robots.txt")
+	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)
+	}
+
+	mapStatic(m, setting.StaticRootPath, "", "public")
+	mapStatic(m, setting.StaticRootPath, "app", "app")
+	mapStatic(m, setting.StaticRootPath, "css", "css")
+	mapStatic(m, setting.StaticRootPath, "img", "img")
+	mapStatic(m, setting.StaticRootPath, "fonts", "fonts")
+	mapStatic(m, setting.StaticRootPath, "robots.txt", "robots.txt")
 
 	m.Use(macaron.Renderer(macaron.RenderOptions{
 		Directory:  path.Join(setting.StaticRootPath, "views"),
@@ -51,7 +58,7 @@ func newMacaron() *macaron.Macaron {
 	return m
 }
 
-func mapStatic(m *macaron.Macaron, dir string, prefix string) {
+func mapStatic(m *macaron.Macaron, rootDir string, dir string, prefix string) {
 	headers := func(c *macaron.Context) {
 		c.Resp.Header().Set("Cache-Control", "public, max-age=3600")
 	}
@@ -63,7 +70,7 @@ func mapStatic(m *macaron.Macaron, dir string, prefix string) {
 	}
 
 	m.Use(httpstatic.Static(
-		path.Join(setting.StaticRootPath, dir),
+		path.Join(rootDir, dir),
 		httpstatic.StaticOptions{
 			SkipLogging: true,
 			Prefix:      prefix,

+ 1 - 1
pkg/login/ldap.go

@@ -131,8 +131,8 @@ func (a *ldapAuther) getGrafanaUserFor(ldapUser *ldapUserInfo) (*m.User, error)
 	}
 
 	return userQuery.Result, nil
-}
 
+}
 func (a *ldapAuther) createGrafanaUser(ldapUser *ldapUserInfo) (*m.User, error) {
 	cmd := m.CreateUserCommand{
 		Login: ldapUser.Username,

+ 34 - 0
pkg/models/plugin_bundle.go

@@ -0,0 +1,34 @@
+package models
+
+import "time"
+
+type PluginBundle struct {
+	Id       int64
+	Type     string
+	OrgId    int64
+	Enabled  bool
+	JsonData map[string]interface{}
+
+	Created time.Time
+	Updated time.Time
+}
+
+// ----------------------
+// COMMANDS
+
+// Also acts as api DTO
+type UpdatePluginBundleCmd struct {
+	Type     string                 `json:"type" binding:"Required"`
+	Enabled  bool                   `json:"enabled"`
+	JsonData map[string]interface{} `json:"jsonData"`
+
+	Id    int64 `json:"-"`
+	OrgId int64 `json:"-"`
+}
+
+// ---------------------
+// QUERIES
+type GetPluginBundlesQuery struct {
+	OrgId  int64
+	Result []*PluginBundle
+}

+ 85 - 0
pkg/plugins/models.go

@@ -0,0 +1,85 @@
+package plugins
+
+import "github.com/grafana/grafana/pkg/models"
+
+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 {
+	Url  string `json:"url"`
+	Path string `json:"path"`
+}
+
+type ExternalPluginRoute 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 ExternalPluginJs struct {
+	Module string `json:"module"`
+}
+
+type ExternalPluginNavLink struct {
+	Text    string          `json:"text"`
+	Icon    string          `json:"icon"`
+	Href    string          `json:"href"`
+	ReqRole models.RoleType `json:"reqRole"`
+}
+
+type ExternalPluginCss struct {
+	Light string `json:"light"`
+	Dark  string `json:"dark"`
+}
+
+type ExternalPlugin struct {
+	Type             string                   `json:"type"`
+	Routes           []*ExternalPluginRoute   `json:"routes"`
+	Js               []*ExternalPluginJs      `json:"js"`
+	Css              []*ExternalPluginCss     `json:"css"`
+	MainNavLinks     []*ExternalPluginNavLink `json:"mainNavLinks"`
+	StaticRootConfig *StaticRootConfig        `json:"staticRoot"`
+}
+
+type PluginBundle struct {
+	Type              string   `json:"type"`
+	Enabled           bool     `json:"enabled"`
+	PanelPlugins      []string `json:"panelPlugins"`
+	DatasourcePlugins []string `json:"datasourcePlugins"`
+	ExternalPlugins   []string `json:"externalPlugins"`
+	Module            string   `json:"module"`
+}
+
+type EnabledPlugins struct {
+	PanelPlugins      []*PanelPlugin
+	DataSourcePlugins map[string]*DataSourcePlugin
+	ExternalPlugins   []*ExternalPlugin
+}
+
+func NewEnabledPlugins() EnabledPlugins {
+	return EnabledPlugins{
+		PanelPlugins:      make([]*PanelPlugin, 0),
+		DataSourcePlugins: make(map[string]*DataSourcePlugin),
+		ExternalPlugins:   make([]*ExternalPlugin, 0),
+	}
+}

+ 149 - 17
pkg/plugins/plugins.go

@@ -6,18 +6,19 @@ import (
 	"os"
 	"path"
 	"path/filepath"
+	"strings"
 
 	"github.com/grafana/grafana/pkg/log"
+	"github.com/grafana/grafana/pkg/models"
 	"github.com/grafana/grafana/pkg/setting"
 )
 
-type PluginMeta struct {
-	Type string `json:"type"`
-	Name string `json:"name"`
-}
-
 var (
-	DataSources map[string]interface{}
+	DataSources     map[string]DataSourcePlugin
+	Panels          map[string]PanelPlugin
+	ExternalPlugins map[string]ExternalPlugin
+	StaticRoutes    []*StaticRootConfig
+	Bundles         map[string]PluginBundle
 )
 
 type PluginScanner struct {
@@ -25,13 +26,53 @@ type PluginScanner struct {
 	errors     []error
 }
 
-func Init() {
+func Init() error {
+	DataSources = make(map[string]DataSourcePlugin)
+	ExternalPlugins = make(map[string]ExternalPlugin)
+	StaticRoutes = make([]*StaticRootConfig, 0)
+	Panels = make(map[string]PanelPlugin)
+	Bundles = make(map[string]PluginBundle)
+
 	scan(path.Join(setting.StaticRootPath, "app/plugins"))
+	checkExternalPluginPaths()
+	checkDependencies()
+	return nil
 }
 
-func scan(pluginDir string) error {
-	DataSources = make(map[string]interface{})
+func checkDependencies() {
+	for bundleType, bundle := range Bundles {
+		for _, reqPanel := range bundle.PanelPlugins {
+			if _, ok := Panels[reqPanel]; !ok {
+				log.Fatal(4, "Bundle %s requires Panel type %s, but it is not present.", bundleType, reqPanel)
+			}
+		}
+		for _, reqDataSource := range bundle.DatasourcePlugins {
+			if _, ok := DataSources[reqDataSource]; !ok {
+				log.Fatal(4, "Bundle %s requires DataSource type %s, but it is not present.", bundleType, reqDataSource)
+			}
+		}
+		for _, reqExtPlugin := range bundle.ExternalPlugins {
+			if _, ok := ExternalPlugins[reqExtPlugin]; !ok {
+				log.Fatal(4, "Bundle %s requires DataSource type %s, but it is not present.", bundleType, reqExtPlugin)
+			}
+		}
+	}
+}
+
+func checkExternalPluginPaths() error {
+	for _, section := range setting.Cfg.Sections() {
+		if strings.HasPrefix(section.Name(), "plugin.") {
+			path := section.Key("path").String()
+			if path != "" {
+				log.Info("Plugin: Scaning dir %s", path)
+				scan(path)
+			}
+		}
+	}
+	return nil
+}
 
+func scan(pluginDir string) error {
 	scanner := &PluginScanner{
 		pluginPath: pluginDir,
 	}
@@ -47,7 +88,7 @@ func scan(pluginDir string) error {
 	return nil
 }
 
-func (scanner *PluginScanner) walker(path string, f os.FileInfo, err error) error {
+func (scanner *PluginScanner) walker(currentPath string, f os.FileInfo, err error) error {
 	if err != nil {
 		return err
 	}
@@ -57,17 +98,25 @@ func (scanner *PluginScanner) walker(path string, f os.FileInfo, err error) erro
 	}
 
 	if f.Name() == "plugin.json" {
-		err := scanner.loadPluginJson(path)
+		err := scanner.loadPluginJson(currentPath)
 		if err != nil {
-			log.Error(3, "Failed to load plugin json file: %v,  err: %v", path, err)
+			log.Error(3, "Failed to load plugin json file: %v,  err: %v", currentPath, err)
 			scanner.errors = append(scanner.errors, err)
 		}
 	}
 	return nil
 }
 
-func (scanner *PluginScanner) loadPluginJson(path string) error {
-	reader, err := os.Open(path)
+func addStaticRoot(staticRootConfig *StaticRootConfig, currentDir string) {
+	if staticRootConfig != nil {
+		staticRootConfig.Path = path.Join(currentDir, staticRootConfig.Path)
+		StaticRoutes = append(StaticRoutes, staticRootConfig)
+	}
+}
+
+func (scanner *PluginScanner) loadPluginJson(pluginJsonFilePath string) error {
+	currentDir := filepath.Dir(pluginJsonFilePath)
+	reader, err := os.Open(pluginJsonFilePath)
 	if err != nil {
 		return err
 	}
@@ -87,12 +136,95 @@ func (scanner *PluginScanner) loadPluginJson(path string) error {
 	}
 
 	if pluginType == "datasource" {
-		datasourceType, exists := pluginJson["type"]
-		if !exists {
+		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)
+	}
+
+	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")
+		}
+
+		Panels[p.Type] = p
+		addStaticRoot(p.StaticRootConfig, currentDir)
+	}
+
+	if pluginType == "external" {
+		p := ExternalPlugin{}
+		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")
+		}
+		ExternalPlugins[p.Type] = p
+		addStaticRoot(p.StaticRootConfig, currentDir)
+	}
+
+	if pluginType == "bundle" {
+		p := PluginBundle{}
+		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[datasourceType.(string)] = pluginJson
+		Bundles[p.Type] = p
 	}
 
 	return nil
 }
+
+func GetEnabledPlugins(orgBundles []*models.PluginBundle) EnabledPlugins {
+	enabledPlugins := NewEnabledPlugins()
+
+	orgBundlesMap := make(map[string]*models.PluginBundle)
+	for _, orgBundle := range orgBundles {
+		orgBundlesMap[orgBundle.Type] = orgBundle
+	}
+
+	for bundleType, bundle := range Bundles {
+		enabled := bundle.Enabled
+		// check if the bundle is stored in the DB.
+		if b, ok := orgBundlesMap[bundleType]; ok {
+			enabled = b.Enabled
+		}
+
+		if enabled {
+			for _, d := range bundle.DatasourcePlugins {
+				if ds, ok := DataSources[d]; ok {
+					enabledPlugins.DataSourcePlugins[d] = &ds
+				}
+			}
+			for _, p := range bundle.PanelPlugins {
+				if panel, ok := Panels[p]; ok {
+					enabledPlugins.PanelPlugins = append(enabledPlugins.PanelPlugins, &panel)
+				}
+			}
+			for _, e := range bundle.ExternalPlugins {
+				if external, ok := ExternalPlugins[e]; ok {
+					enabledPlugins.ExternalPlugins = append(enabledPlugins.ExternalPlugins, &external)
+				}
+			}
+		}
+	}
+	return enabledPlugins
+}

+ 5 - 2
pkg/plugins/plugins_test.go

@@ -4,14 +4,17 @@ import (
 	"path/filepath"
 	"testing"
 
+	"github.com/grafana/grafana/pkg/setting"
 	. "github.com/smartystreets/goconvey/convey"
+	"gopkg.in/ini.v1"
 )
 
 func TestPluginScans(t *testing.T) {
 
 	Convey("When scaning for plugins", t, func() {
-		path, _ := filepath.Abs("../../public/app/plugins")
-		err := scan(path)
+		setting.StaticRootPath, _ = filepath.Abs("../../public/")
+		setting.Cfg = ini.Empty()
+		err := Init()
 
 		So(err, ShouldBeNil)
 		So(len(DataSources), ShouldBeGreaterThan, 1)

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

@@ -18,6 +18,7 @@ func AddMigrations(mg *Migrator) {
 	addApiKeyMigrations(mg)
 	addDashboardSnapshotMigrations(mg)
 	addQuotaMigration(mg)
+	addPluginBundleMigration(mg)
 }
 
 func addMigrationLogMigrations(mg *Migrator) {

+ 26 - 0
pkg/services/sqlstore/migrations/plugin_bundle.go

@@ -0,0 +1,26 @@
+package migrations
+
+import . "github.com/grafana/grafana/pkg/services/sqlstore/migrator"
+
+func addPluginBundleMigration(mg *Migrator) {
+
+	var pluginBundleV1 = Table{
+		Name: "plugin_bundle",
+		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: "enabled", 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},
+		},
+	}
+	mg.AddMigration("create plugin_bundle table v1", NewAddTableMigration(pluginBundleV1))
+
+	//-------  indexes ------------------
+	addTableIndicesMigrations(mg, "v1", pluginBundleV1)
+}

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

@@ -0,0 +1,46 @@
+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
+		}
+	})
+}

+ 2 - 4
pkg/setting/setting.go

@@ -287,13 +287,11 @@ func loadSpecifedConfigFile(configFile string) {
 
 			defaultSec, err := Cfg.GetSection(section.Name())
 			if err != nil {
-				log.Error(3, "Unknown config section %s defined in %s", section.Name(), configFile)
-				continue
+				defaultSec, _ = Cfg.NewSection(section.Name())
 			}
 			defaultKey, err := defaultSec.GetKey(key.Name())
 			if err != nil {
-				log.Error(3, "Unknown config key %s defined in section %s, in file %s", key.Name(), section.Name(), configFile)
-				continue
+				defaultKey, _ = defaultSec.NewKey(key.Name(), key.Value())
 			}
 			defaultKey.SetValue(key.Value())
 		}

+ 11 - 6
public/app/app.js

@@ -2,6 +2,7 @@ define([
   'angular',
   'jquery',
   'lodash',
+  'app/core/config',
   'require',
   'bootstrap',
   'angular-route',
@@ -12,7 +13,7 @@ define([
   'bindonce',
   'app/core/core',
 ],
-function (angular, $, _, appLevelRequire) {
+function (angular, $, _, config, appLevelRequire) {
   "use strict";
 
   var app = angular.module('grafana', []);
@@ -35,6 +36,8 @@ function (angular, $, _, appLevelRequire) {
     } else {
       _.extend(module, register_fns);
     }
+    // push it into the apps dependencies
+    apps_deps.push(module.name);
     return module;
   };
 
@@ -64,13 +67,15 @@ function (angular, $, _, appLevelRequire) {
     var module_name = 'grafana.'+type;
     // create the module
     app.useModule(angular.module(module_name, []));
-    // push it into the apps dependencies
-    apps_deps.push(module_name);
   });
 
-  var preBootRequires = [
-    'app/features/all',
-  ];
+  var preBootRequires = ['app/features/all'];
+  var pluginModules = config.bootData.pluginModules || [];
+
+  // add plugin modules
+  for (var i = 0; i < pluginModules.length; i++) {
+    preBootRequires.push(pluginModules[i]);
+  }
 
   app.boot = function() {
     require(preBootRequires, function () {

+ 1 - 0
public/app/core/config.js

@@ -6,6 +6,7 @@ function (Settings) {
 
   var bootData = window.grafanaBootData || { settings: {} };
   var options = bootData.settings;
+  options.bootData = bootData;
 
   return new Settings(options);
 

+ 5 - 11
public/app/core/controllers/sidemenu_ctrl.js

@@ -15,19 +15,13 @@ function (angular, _, $, coreModule, config) {
     };
 
     $scope.setupMainNav = function() {
-      $scope.mainLinks.push({
-        text: "Dashboards",
-        icon: "fa fa-fw fa-th-large",
-        href: $scope.getUrl("/"),
-      });
-
-      if (contextSrv.hasRole('Admin')) {
+      _.each(config.bootData.mainNavLinks, function(item) {
         $scope.mainLinks.push({
-          text: "Data Sources",
-          icon: "fa fa-fw fa-database",
-          href: $scope.getUrl("/datasources"),
+          text: item.text,
+          icon: item.icon,
+          href: $scope.getUrl(item.href)
         });
-      }
+      });
     };
 
     $scope.loadOrgs = function() {

+ 10 - 0
public/app/core/routes/all.js

@@ -131,6 +131,16 @@ 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('/global-alerts', {
         templateUrl: 'app/features/dashboard/partials/globalAlerts.html',
       })

+ 2 - 2
public/app/core/services/context_srv.js

@@ -12,8 +12,8 @@ function (angular, _, coreModule, store, config) {
     var self = this;
 
     function User() {
-      if (window.grafanaBootData.user) {
-        _.extend(this, window.grafanaBootData.user);
+      if (config.bootData.user) {
+        _.extend(this, config.bootData.user);
       }
     }
 

+ 1 - 8
public/app/core/settings.js

@@ -8,15 +8,8 @@ function (_) {
     var defaults = {
       datasources                   : {},
       window_title_prefix           : 'Grafana - ',
-      panels                        : {
-        'graph':      { path: 'app/panels/graph',      name: 'Graph' },
-        'table':      { path: 'app/panels/table',      name: 'Table' },
-        'singlestat': { path: 'app/panels/singlestat', name: 'Single stat' },
-        'text':       { path: 'app/panels/text',       name: 'Text' },
-        'dashlist':   { path: 'app/panels/dashlist',   name: 'Dashboard list' },
-      },
+      panels                        : {},
       new_panel_title: 'Panel Title',
-      plugins: {},
       playlist_timespan: "1m",
       unsaved_changes_warning: true,
       appSubUrl: ""

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

@@ -6,4 +6,8 @@ define([
   './userInviteCtrl',
   './orgApiKeysCtrl',
   './orgDetailsCtrl',
+  './pluginsCtrl',
+  './pluginEditCtrl',
+  './plugin_srv',
+  './plugin_directive',
 ], function () {});

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

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

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

@@ -0,0 +1,42 @@
+<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>

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

@@ -0,0 +1,41 @@
+<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>

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

@@ -0,0 +1,35 @@
+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();
+  });
+});

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

@@ -0,0 +1,47 @@
+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();
+        };
+      }
+    };
+  });
+});

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

@@ -0,0 +1,58 @@
+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();
+  });
+});

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

@@ -0,0 +1,33 @@
+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();
+
+  });
+});

+ 2 - 2
public/app/features/panel/panel_directive.js

@@ -13,9 +13,9 @@ function (angular, $, config) {
       restrict: 'E',
       link: function(scope, elem, attr) {
         var getter = $parse(attr.type), panelType = getter(scope);
-        var panelPath = config.panels[panelType].path;
+        var module = config.panels[panelType].module;
 
-        scope.require([panelPath + "/module"], function () {
+        scope.require([module], function () {
           var panelEl = angular.element(document.createElement('grafana-panel-' + panelType));
           elem.append(panelEl);
           $compile(panelEl)(scope);

+ 13 - 0
public/app/plugins/external/example/readme.md

@@ -0,0 +1,13 @@
+Example app is available at https://github.com/raintank/grafana-plugin-example
+
+* Clone plugin repo git@github.com:raintank/grafana-plugin-example.git
+
+* Modify grafana.ini (or custom.ini if your developing Grafana locally)
+
+```ini
+[plugin.external-test]
+path = /<the_path_were_you_cloned_it>/grafana-plugin-example
+```
+
+
+

+ 0 - 0
public/app/panels/dashlist/editor.html → public/app/plugins/panels/dashlist/editor.html


+ 0 - 0
public/app/panels/dashlist/module.html → public/app/plugins/panels/dashlist/module.html


+ 2 - 2
public/app/panels/dashlist/module.js → public/app/plugins/panels/dashlist/module.js

@@ -14,7 +14,7 @@ function (angular, app, _, config, PanelMeta) {
   module.directive('grafanaPanelDashlist', function() {
     return {
       controller: 'DashListPanelCtrl',
-      templateUrl: 'app/panels/dashlist/module.html',
+      templateUrl: 'app/plugins/panels/dashlist/module.html',
     };
   });
 
@@ -26,7 +26,7 @@ function (angular, app, _, config, PanelMeta) {
       fullscreen: true,
     });
 
-    $scope.panelMeta.addEditorTab('Options', 'app/panels/dashlist/editor.html');
+    $scope.panelMeta.addEditorTab('Options', 'app/plugins/panels/dashlist/editor.html');
 
     var defaults = {
       mode: 'starred',

+ 8 - 0
public/app/plugins/panels/dashlist/plugin.json

@@ -0,0 +1,8 @@
+{
+  "pluginType": "panel",
+
+  "name": "Dashboard list",
+  "type": "dashlist",
+
+  "module": "app/plugins/panels/dashlist/module"
+}

+ 0 - 0
public/app/panels/graph/axisEditor.html → public/app/plugins/panels/graph/axisEditor.html


+ 0 - 0
public/app/panels/graph/graph.js → public/app/plugins/panels/graph/graph.js


+ 0 - 0
public/app/panels/graph/graph.tooltip.js → public/app/plugins/panels/graph/graph.tooltip.js


+ 1 - 1
public/app/panels/graph/legend.js → public/app/plugins/panels/graph/legend.js

@@ -45,7 +45,7 @@ function (angular, _, $) {
           popoverScope.series = seriesInfo;
           popoverSrv.show({
             element: el,
-            templateUrl:  'app/panels/graph/legend.popover.html',
+            templateUrl:  'app/plugins/panels/graph/legend.popover.html',
             scope: popoverScope
           });
         }

+ 0 - 0
public/app/panels/graph/legend.popover.html → public/app/plugins/panels/graph/legend.popover.html


+ 0 - 0
public/app/panels/graph/module.html → public/app/plugins/panels/graph/module.html


+ 3 - 3
public/app/panels/graph/module.js → public/app/plugins/panels/graph/module.js

@@ -17,7 +17,7 @@ function (angular, _, moment, kbn, TimeSeries, PanelMeta) {
   module.directive('grafanaPanelGraph', function() {
     return {
       controller: 'GraphCtrl',
-      templateUrl: 'app/panels/graph/module.html',
+      templateUrl: 'app/plugins/panels/graph/module.html',
     };
   });
 
@@ -30,8 +30,8 @@ function (angular, _, moment, kbn, TimeSeries, PanelMeta) {
       metricsEditor: true,
     });
 
-    $scope.panelMeta.addEditorTab('Axes & Grid', 'app/panels/graph/axisEditor.html');
-    $scope.panelMeta.addEditorTab('Display Styles', 'app/panels/graph/styleEditor.html');
+    $scope.panelMeta.addEditorTab('Axes & Grid', 'app/plugins/panels/graph/axisEditor.html');
+    $scope.panelMeta.addEditorTab('Display Styles', 'app/plugins/panels/graph/styleEditor.html');
     $scope.panelMeta.addEditorTab('Time range', 'app/features/panel/partials/panelTime.html');
 
     $scope.panelMeta.addExtendedMenuItem('Export CSV', '', 'exportCsv()');

+ 8 - 0
public/app/plugins/panels/graph/plugin.json

@@ -0,0 +1,8 @@
+{
+  "pluginType": "panel",
+
+  "name": "Graph",
+  "type": "graph",
+
+  "module": "app/plugins/panels/graph/module"
+}

+ 0 - 0
public/app/panels/graph/seriesOverridesCtrl.js → public/app/plugins/panels/graph/seriesOverridesCtrl.js


+ 0 - 0
public/app/panels/graph/styleEditor.html → public/app/plugins/panels/graph/styleEditor.html


+ 0 - 0
public/app/panels/singlestat/editor.html → public/app/plugins/panels/singlestat/editor.html


+ 0 - 0
public/app/panels/singlestat/module.html → public/app/plugins/panels/singlestat/module.html


+ 2 - 2
public/app/panels/singlestat/module.js → public/app/plugins/panels/singlestat/module.js

@@ -16,7 +16,7 @@ function (angular, app, _, kbn, TimeSeries, PanelMeta) {
   module.directive('grafanaPanelSinglestat', function() {
     return {
       controller: 'SingleStatCtrl',
-      templateUrl: 'app/panels/singlestat/module.html',
+      templateUrl: 'app/plugins/panels/singlestat/module.html',
     };
   });
 
@@ -31,7 +31,7 @@ function (angular, app, _, kbn, TimeSeries, PanelMeta) {
 
     $scope.fontSizes = ['20%', '30%','50%','70%','80%','100%', '110%', '120%', '150%', '170%', '200%'];
 
-    $scope.panelMeta.addEditorTab('Options', 'app/panels/singlestat/editor.html');
+    $scope.panelMeta.addEditorTab('Options', 'app/plugins/panels/singlestat/editor.html');
     $scope.panelMeta.addEditorTab('Time range', 'app/features/panel/partials/panelTime.html');
 
     // Set and populate defaults

+ 8 - 0
public/app/plugins/panels/singlestat/plugin.json

@@ -0,0 +1,8 @@
+{
+  "pluginType": "panel",
+
+  "name": "Singlestat",
+  "type": "singlestat",
+
+  "module": "app/plugins/panels/singlestat/module"
+}

+ 0 - 0
public/app/panels/singlestat/singleStatPanel.js → public/app/plugins/panels/singlestat/singleStatPanel.js


+ 2 - 2
public/app/panels/table/controller.ts → public/app/plugins/panels/table/controller.ts

@@ -1,4 +1,4 @@
-///<reference path="../../headers/common.d.ts" />
+///<reference path="../../../headers/common.d.ts" />
 
 import angular = require('angular');
 import _ = require('lodash');
@@ -21,7 +21,7 @@ export class TablePanelCtrl {
       metricsEditor: true,
     });
 
-    $scope.panelMeta.addEditorTab('Options', 'app/panels/table/options.html');
+    $scope.panelMeta.addEditorTab('Options', 'app/plugins/panels/table/options.html');
     $scope.panelMeta.addEditorTab('Time range', 'app/features/panel/partials/panelTime.html');
 
     var panelDefaults = {

+ 0 - 0
public/app/panels/table/editor.html → public/app/plugins/panels/table/editor.html


+ 1 - 3
public/app/panels/table/editor.ts → public/app/plugins/panels/table/editor.ts

@@ -1,5 +1,4 @@
-///<reference path="../../headers/common.d.ts" />
-
+///<reference path="../../../headers/common.d.ts" />
 
 import angular = require('angular');
 import $ = require('jquery');
@@ -122,4 +121,3 @@ export function tablePanelEditor($q, uiSegmentSrv) {
     controller: TablePanelEditorCtrl,
   };
 }
-

+ 0 - 0
public/app/panels/table/module.html → public/app/plugins/panels/table/module.html


+ 2 - 2
public/app/panels/table/module.ts → public/app/plugins/panels/table/module.ts

@@ -1,4 +1,4 @@
-///<reference path="../../headers/common.d.ts" />
+///<reference path="../../../headers/common.d.ts" />
 
 import angular = require('angular');
 import $ = require('jquery');
@@ -14,7 +14,7 @@ export function tablePanel() {
   'use strict';
   return {
     restrict: 'E',
-    templateUrl: 'app/panels/table/module.html',
+    templateUrl: 'app/plugins/panels/table/module.html',
     controller: TablePanelCtrl,
     link: function(scope, elem) {
       var data;

+ 0 - 0
public/app/panels/table/options.html → public/app/plugins/panels/table/options.html


+ 8 - 0
public/app/plugins/panels/table/plugin.json

@@ -0,0 +1,8 @@
+{
+  "pluginType": "panel",
+
+  "name": "Table",
+  "type": "table",
+
+  "module": "app/plugins/panels/table/module"
+}

+ 1 - 1
public/app/panels/table/renderer.ts → public/app/plugins/panels/table/renderer.ts

@@ -1,4 +1,4 @@
-///<reference path="../../headers/common.d.ts" />
+///<reference path="../../../headers/common.d.ts" />
 
 import _ = require('lodash');
 import kbn = require('app/core/utils/kbn');

+ 0 - 0
public/app/panels/table/specs/renderer_specs.ts → public/app/plugins/panels/table/specs/renderer_specs.ts


+ 0 - 0
public/app/panels/table/specs/table_model_specs.ts → public/app/plugins/panels/table/specs/table_model_specs.ts


+ 0 - 0
public/app/panels/table/specs/transformers_specs.ts → public/app/plugins/panels/table/specs/transformers_specs.ts


+ 52 - 0
public/app/plugins/panels/table/table_model.ts

@@ -0,0 +1,52 @@
+import {transformers} from './transformers';
+
+export class TableModel {
+  columns: any[];
+  rows: any[];
+
+  constructor() {
+    this.columns = [];
+    this.rows = [];
+  }
+
+  sort(options) {
+    if (options.col === null || this.columns.length <= options.col) {
+      return;
+    }
+
+    this.rows.sort(function(a, b) {
+      a = a[options.col];
+      b = b[options.col];
+      if (a < b) {
+        return -1;
+      }
+      if (a > b) {
+        return 1;
+      }
+      return 0;
+    });
+
+    this.columns[options.col].sort = true;
+
+    if (options.desc) {
+      this.rows.reverse();
+      this.columns[options.col].desc = true;
+    }
+  }
+
+  static transform(data, panel) {
+    var model = new TableModel();
+
+    if (!data || data.length === 0) {
+      return model;
+    }
+
+    var transformer = transformers[panel.transform];
+    if (!transformer) {
+      throw {message: 'Transformer ' + panel.transformer + ' not found'};
+    }
+
+    transformer.transform(data, panel, model);
+    return model;
+  }
+}

+ 1 - 1
public/app/panels/table/transformers.ts → public/app/plugins/panels/table/transformers.ts

@@ -1,4 +1,4 @@
-///<reference path="../../headers/common.d.ts" />
+///<reference path="../../../headers/common.d.ts" />
 
 import moment = require('moment');
 import _ = require('lodash');

+ 0 - 0
public/app/panels/text/editor.html → public/app/plugins/panels/text/editor.html


+ 0 - 0
public/app/panels/text/module.html → public/app/plugins/panels/text/module.html


+ 3 - 3
public/app/panels/text/module.js → public/app/plugins/panels/text/module.js

@@ -16,7 +16,7 @@ function (angular, app, _, require, PanelMeta) {
   module.directive('grafanaPanelText', function() {
     return {
       controller: 'TextPanelCtrl',
-      templateUrl: 'app/panels/text/module.html',
+      templateUrl: 'app/plugins/panels/text/module.html',
     };
   });
 
@@ -28,7 +28,7 @@ function (angular, app, _, require, PanelMeta) {
       fullscreen: true,
     });
 
-    $scope.panelMeta.addEditorTab('Edit text', 'app/panels/text/editor.html');
+    $scope.panelMeta.addEditorTab('Edit text', 'app/plugins/panels/text/editor.html');
 
     // Set and populate defaults
     var _d = {
@@ -84,7 +84,7 @@ function (angular, app, _, require, PanelMeta) {
         $scope.updateContent(converter.makeHtml(text));
       }
       else {
-        require(['./lib/showdown'], function (Showdown) {
+        require(['vendor/showdown'], function (Showdown) {
           converter = new Showdown.converter();
           $scope.updateContent(converter.makeHtml(text));
         });

+ 8 - 0
public/app/plugins/panels/text/plugin.json

@@ -0,0 +1,8 @@
+{
+  "pluginType": "panel",
+
+  "name": "Text",
+  "type": "text",
+
+  "module": "app/plugins/panels/text/module"
+}

+ 9 - 0
public/app/plugins/plugin.json

@@ -0,0 +1,9 @@
+{
+  "pluginType": "bundle",
+  "type": "core",
+  "module": "",
+  "enabled": true,
+  "panelPlugins": ["graph", "singlestat", "text", "dashlist", "table"],
+  "datasourcePlugins": ["mixed", "grafana", "graphite", "cloudwatch", "elasticsearch", "influxdb", "influxdb_08", "kairosdb", "opentsdb", "prometheus"],
+  "externalPlugins": []
+}

+ 0 - 0
public/app/plugins/PLUGIN_CHANGES.md → public/app/plugins/plugin_api.md


+ 1 - 1
public/test/specs/graph-ctrl-specs.js

@@ -2,7 +2,7 @@ define([
   './helpers',
   'app/features/panel/panel_srv',
   'app/features/panel/panel_helper',
-  'app/panels/graph/module'
+  'app/plugins/panels/graph/module'
 ], function(helpers) {
   'use strict';
 

+ 1 - 1
public/test/specs/graph-specs.js

@@ -3,7 +3,7 @@ define([
   'angular',
   'jquery',
   'app/core/time_series',
-  'app/panels/graph/graph'
+  'app/plugins/panels/graph/graph'
 ], function(helpers, angular, $, TimeSeries) {
   'use strict';
 

+ 1 - 1
public/test/specs/graph-tooltip-specs.js

@@ -1,6 +1,6 @@
 define([
   'jquery',
-  'app/panels/graph/graph.tooltip'
+  'app/plugins/panels/graph/graph.tooltip'
 ], function($, GraphTooltip) {
   'use strict';
 

+ 1 - 1
public/test/specs/seriesOverridesCtrl-specs.js

@@ -1,6 +1,6 @@
 define([
   './helpers',
-  'app/panels/graph/seriesOverridesCtrl'
+  'app/plugins/panels/graph/seriesOverridesCtrl'
 ], function(helpers) {
   'use strict';
 

+ 1 - 1
public/test/specs/singlestat-specs.js

@@ -2,7 +2,7 @@ define([
   './helpers',
   'app/features/panel/panel_srv',
   'app/features/panel/panel_helper',
-  'app/panels/singlestat/module'
+  'app/plugins/panels/singlestat/module'
 ], function(helpers) {
   'use strict';
 

+ 2 - 0
public/test/test-main.js

@@ -95,6 +95,8 @@ function file2moduleName(filePath) {
     .replace(/\.\w*$/, '');
 }
 
+window.grafanaBootData = {settings: {}};
+
 require([
   'lodash',
   'angular',

+ 0 - 0
public/app/panels/text/lib/showdown.js → public/vendor/showdown.js


+ 11 - 1
public/views/index.html

@@ -10,10 +10,18 @@
 
 		[[if .User.LightTheme]]
 		  <link rel="stylesheet" href="[[.AppSubUrl]]/public/css/grafana.light.min.css">
+		  [[ range $css := .PluginCss ]]
+			<link rel="stylesheet" href="[[$.AppSubUrl]]/[[ $css.Light ]]">
+		  [[ end ]]
 		[[else]]
 		  <link rel="stylesheet" href="[[.AppSubUrl]]/public/css/grafana.dark.min.css">
+		  [[ range $css := .PluginCss ]]
+			<link rel="stylesheet" href="[[$.AppSubUrl]]/[[ $css.Dark ]]">
+		  [[ end ]]
 		[[end]]
 
+		
+
     <link rel="icon" type="image/png" href="[[.AppSubUrl]]/public/img/fav32.png">
 		<base href="[[.AppSubUrl]]/" />
 
@@ -50,10 +58,12 @@
 		window.grafanaBootData = {
 			user:[[.User]],
 			settings: [[.Settings]],
+			pluginModules: [[.PluginJs]],
+			mainNavLinks: [[.MainNavLinks]]
 		};
 
     require(['app/app'], function (app) {
-	    app.boot();
+    	app.boot();
     })
 	</script>
 

+ 1 - 3
tasks/options/htmlmin.js

@@ -8,9 +8,7 @@ module.exports = function(config) {
       expand: true,
       cwd: '<%= genDir %>',
       src: [
-        //'index.html',
-        'app/panels/**/*.html',
-        'app/partials/**/*.html'
+        'app/**/*.html',
       ],
       dest: '<%= genDir %>'
     }

+ 1 - 2
tasks/options/jscs.js

@@ -4,7 +4,6 @@ module.exports = function(config) {
       'Gruntfile.js',
       '<%= srcDir %>/app/**/*.js',
       '<%= srcDir %>/plugins/**/*.js',
-      '!<%= srcDir %>/app/panels/*/{lib,leaflet}/*',
       '!<%= srcDir %>/app/dashboards/*'
     ],
     options: {
@@ -20,4 +19,4 @@ module.exports = function(config) {
     "disallowRightStickedOperators": ["?", "+", "/", "*", ":", "=", "==", "===", "!=", "!==", ">", ">=", "<", "<="],
     "requireRightStickedOperators": ["!"],
     "requireLeftStickedOperators": [","],
-   */
+   */

+ 1 - 2
tasks/options/jshint.js

@@ -18,9 +18,8 @@ module.exports = function(config) {
         'dist/*',
         'sample/*',
         '<%= srcDir %>/vendor/*',
-        '<%= srcDir %>/app/panels/*/{lib,leaflet}/*',
         '<%= srcDir %>/app/dashboards/*'
       ]
     }
   };
-};
+};

+ 2 - 2
tasks/options/requirejs.js

@@ -62,11 +62,11 @@ module.exports = function(config,grunt) {
     ];
 
     var fs = require('fs');
-    var panelPath = config.srcDir + '/app/panels';
+    var panelPath = config.srcDir + '/app/plugins/panels';
 
     // create a module for each directory in public/app/panels/
     fs.readdirSync(panelPath).forEach(function (panelName) {
-      requireModules[0].include.push('app/panels/'+panelName+'/module');
+      requireModules[0].include.push('app/plugins/panels/'+panelName+'/module');
     });
 
     return { options: options };