mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Merge branch 'external-plugins'
This commit is contained in:
commit
2ec5bc77d7
@ -13,7 +13,7 @@ func Register(r *macaron.Macaron) {
|
|||||||
reqSignedIn := middleware.Auth(&middleware.AuthOptions{ReqSignedIn: true})
|
reqSignedIn := middleware.Auth(&middleware.AuthOptions{ReqSignedIn: true})
|
||||||
reqGrafanaAdmin := middleware.Auth(&middleware.AuthOptions{ReqSignedIn: true, ReqGrafanaAdmin: true})
|
reqGrafanaAdmin := middleware.Auth(&middleware.AuthOptions{ReqSignedIn: true, ReqGrafanaAdmin: true})
|
||||||
reqEditorRole := middleware.RoleAuth(m.ROLE_EDITOR, m.ROLE_ADMIN)
|
reqEditorRole := middleware.RoleAuth(m.ROLE_EDITOR, m.ROLE_ADMIN)
|
||||||
regOrgAdmin := middleware.RoleAuth(m.ROLE_ADMIN)
|
reqOrgAdmin := middleware.RoleAuth(m.ROLE_ADMIN)
|
||||||
quota := middleware.Quota
|
quota := middleware.Quota
|
||||||
bind := binding.Bind
|
bind := binding.Bind
|
||||||
|
|
||||||
@ -41,6 +41,9 @@ func Register(r *macaron.Macaron) {
|
|||||||
r.Get("/admin/orgs", reqGrafanaAdmin, Index)
|
r.Get("/admin/orgs", reqGrafanaAdmin, Index)
|
||||||
r.Get("/admin/orgs/edit/:id", 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/*", reqSignedIn, Index)
|
||||||
r.Get("/dashboard-solo/*", reqSignedIn, Index)
|
r.Get("/dashboard-solo/*", reqSignedIn, Index)
|
||||||
|
|
||||||
@ -114,7 +117,7 @@ func Register(r *macaron.Macaron) {
|
|||||||
r.Get("/invites", wrap(GetPendingOrgInvites))
|
r.Get("/invites", wrap(GetPendingOrgInvites))
|
||||||
r.Post("/invites", quota("user"), bind(dtos.AddInviteForm{}), wrap(AddOrgInvite))
|
r.Post("/invites", quota("user"), bind(dtos.AddInviteForm{}), wrap(AddOrgInvite))
|
||||||
r.Patch("/invites/:code/revoke", wrap(RevokeInvite))
|
r.Patch("/invites/:code/revoke", wrap(RevokeInvite))
|
||||||
}, regOrgAdmin)
|
}, reqOrgAdmin)
|
||||||
|
|
||||||
// create new org
|
// create new org
|
||||||
r.Post("/orgs", quota("org"), bind(m.CreateOrgCommand{}), wrap(CreateOrg))
|
r.Post("/orgs", quota("org"), bind(m.CreateOrgCommand{}), wrap(CreateOrg))
|
||||||
@ -141,7 +144,7 @@ func Register(r *macaron.Macaron) {
|
|||||||
r.Get("/", wrap(GetApiKeys))
|
r.Get("/", wrap(GetApiKeys))
|
||||||
r.Post("/", quota("api_key"), bind(m.AddApiKeyCommand{}), wrap(AddApiKey))
|
r.Post("/", quota("api_key"), bind(m.AddApiKeyCommand{}), wrap(AddApiKey))
|
||||||
r.Delete("/:id", wrap(DeleteApiKey))
|
r.Delete("/:id", wrap(DeleteApiKey))
|
||||||
}, regOrgAdmin)
|
}, reqOrgAdmin)
|
||||||
|
|
||||||
// Data sources
|
// Data sources
|
||||||
r.Group("/datasources", func() {
|
r.Group("/datasources", func() {
|
||||||
@ -151,7 +154,13 @@ func Register(r *macaron.Macaron) {
|
|||||||
r.Delete("/:id", DeleteDataSource)
|
r.Delete("/:id", DeleteDataSource)
|
||||||
r.Get("/:id", wrap(GetDataSourceById))
|
r.Get("/:id", wrap(GetDataSourceById))
|
||||||
r.Get("/plugins", GetDataSourcePlugins)
|
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.Get("/frontend/settings/", GetFrontendSettings)
|
||||||
r.Any("/datasources/proxy/:id/*", reqSignedIn, ProxyDataSourceRequest)
|
r.Any("/datasources/proxy/:id/*", reqSignedIn, ProxyDataSourceRequest)
|
||||||
@ -188,5 +197,7 @@ func Register(r *macaron.Macaron) {
|
|||||||
// rendering
|
// rendering
|
||||||
r.Get("/render/*", reqSignedIn, RenderToPng)
|
r.Get("/render/*", reqSignedIn, RenderToPng)
|
||||||
|
|
||||||
|
InitExternalPluginRoutes(r)
|
||||||
|
|
||||||
r.NotFound(NotFoundHandler)
|
r.NotFound(NotFoundHandler)
|
||||||
}
|
}
|
||||||
|
@ -117,8 +117,15 @@ func UpdateDataSource(c *middleware.Context, cmd m.UpdateDataSourceCommand) {
|
|||||||
func GetDataSourcePlugins(c *middleware.Context) {
|
func GetDataSourcePlugins(c *middleware.Context) {
|
||||||
dsList := make(map[string]interface{})
|
dsList := make(map[string]interface{})
|
||||||
|
|
||||||
for key, value := range plugins.DataSources {
|
orgBundles := m.GetPluginBundlesQuery{OrgId: c.OrgId}
|
||||||
if value.(map[string]interface{})["builtIn"] == nil {
|
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
|
dsList[key] = value
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
25
pkg/api/dtos/index.go
Normal file
25
pkg/api/dtos/index.go
Normal file
@ -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
pkg/api/dtos/plugin_bundle.go
Normal file
8
pkg/api/dtos/plugin_bundle.go
Normal file
@ -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
pkg/api/externalplugin.go
Normal file
75
pkg/api/externalplugin.go
Normal file
@ -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}
|
||||||
|
}
|
@ -29,6 +29,13 @@ func getFrontendSettingsMap(c *middleware.Context) (map[string]interface{}, erro
|
|||||||
datasources := make(map[string]interface{})
|
datasources := make(map[string]interface{})
|
||||||
var defaultDatasource string
|
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 {
|
for _, ds := range orgDataSources {
|
||||||
url := ds.Url
|
url := ds.Url
|
||||||
|
|
||||||
@ -42,7 +49,7 @@ func getFrontendSettingsMap(c *middleware.Context) (map[string]interface{}, erro
|
|||||||
"url": url,
|
"url": url,
|
||||||
}
|
}
|
||||||
|
|
||||||
meta, exists := plugins.DataSources[ds.Type]
|
meta, exists := enabledPlugins.DataSourcePlugins[ds.Type]
|
||||||
if !exists {
|
if !exists {
|
||||||
log.Error(3, "Could not find plugin definition for data source: %v", ds.Type)
|
log.Error(3, "Could not find plugin definition for data source: %v", ds.Type)
|
||||||
continue
|
continue
|
||||||
@ -109,9 +116,18 @@ func getFrontendSettingsMap(c *middleware.Context) (map[string]interface{}, erro
|
|||||||
defaultDatasource = "-- Grafana --"
|
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{}{
|
jsonObj := map[string]interface{}{
|
||||||
"defaultDatasource": defaultDatasource,
|
"defaultDatasource": defaultDatasource,
|
||||||
"datasources": datasources,
|
"datasources": datasources,
|
||||||
|
"panels": panels,
|
||||||
"appSubUrl": setting.AppSubUrl,
|
"appSubUrl": setting.AppSubUrl,
|
||||||
"allowOrgCreate": (setting.AllowUserOrgCreate && c.IsSignedIn) || c.IsGrafanaAdmin,
|
"allowOrgCreate": (setting.AllowUserOrgCreate && c.IsSignedIn) || c.IsGrafanaAdmin,
|
||||||
"authProxyEnabled": setting.AuthProxyEnabled,
|
"authProxyEnabled": setting.AuthProxyEnabled,
|
||||||
|
@ -2,17 +2,21 @@ package api
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/grafana/grafana/pkg/api/dtos"
|
"github.com/grafana/grafana/pkg/api/dtos"
|
||||||
|
"github.com/grafana/grafana/pkg/bus"
|
||||||
"github.com/grafana/grafana/pkg/middleware"
|
"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"
|
"github.com/grafana/grafana/pkg/setting"
|
||||||
)
|
)
|
||||||
|
|
||||||
func setIndexViewData(c *middleware.Context) error {
|
func setIndexViewData(c *middleware.Context) (*dtos.IndexViewData, error) {
|
||||||
settings, err := getFrontendSettingsMap(c)
|
settings, err := getFrontendSettingsMap(c)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
currentUser := &dtos.CurrentUser{
|
var data = dtos.IndexViewData{
|
||||||
|
User: &dtos.CurrentUser{
|
||||||
Id: c.UserId,
|
Id: c.UserId,
|
||||||
IsSignedIn: c.IsSignedIn,
|
IsSignedIn: c.IsSignedIn,
|
||||||
Login: c.Login,
|
Login: c.Login,
|
||||||
@ -24,44 +28,95 @@ func setIndexViewData(c *middleware.Context) error {
|
|||||||
OrgRole: c.OrgRole,
|
OrgRole: c.OrgRole,
|
||||||
GravatarUrl: dtos.GetGravatarUrl(c.Email),
|
GravatarUrl: dtos.GetGravatarUrl(c.Email),
|
||||||
IsGrafanaAdmin: c.IsGrafanaAdmin,
|
IsGrafanaAdmin: c.IsGrafanaAdmin,
|
||||||
|
},
|
||||||
|
Settings: settings,
|
||||||
|
AppUrl: setting.AppUrl,
|
||||||
|
AppSubUrl: setting.AppSubUrl,
|
||||||
|
GoogleAnalyticsId: setting.GoogleAnalyticsId,
|
||||||
|
GoogleTagManagerId: setting.GoogleTagManagerId,
|
||||||
}
|
}
|
||||||
|
|
||||||
if setting.DisableGravatar {
|
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 {
|
if len(data.User.Name) == 0 {
|
||||||
currentUser.Name = currentUser.Login
|
data.User.Name = data.User.Login
|
||||||
}
|
}
|
||||||
|
|
||||||
themeUrlParam := c.Query("theme")
|
themeUrlParam := c.Query("theme")
|
||||||
if themeUrlParam == "light" {
|
if themeUrlParam == "light" {
|
||||||
currentUser.LightTheme = true
|
data.User.LightTheme = true
|
||||||
}
|
}
|
||||||
|
|
||||||
c.Data["User"] = currentUser
|
data.MainNavLinks = append(data.MainNavLinks, &dtos.NavLink{
|
||||||
c.Data["Settings"] = settings
|
Text: "Dashboards",
|
||||||
c.Data["AppUrl"] = setting.AppUrl
|
Icon: "fa fa-fw fa-th-large",
|
||||||
c.Data["AppSubUrl"] = setting.AppSubUrl
|
Href: "/",
|
||||||
|
})
|
||||||
|
|
||||||
if setting.GoogleAnalyticsId != "" {
|
if c.OrgRole == m.ROLE_ADMIN {
|
||||||
c.Data["GoogleAnalyticsId"] = setting.GoogleAnalyticsId
|
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 != "" {
|
orgBundles := m.GetPluginBundlesQuery{OrgId: c.OrgId}
|
||||||
c.Data["GoogleTagManagerId"] = setting.GoogleTagManagerId
|
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) {
|
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)
|
c.Handle(500, "Failed to get settings", err)
|
||||||
return
|
return
|
||||||
|
} else {
|
||||||
|
c.HTML(200, "index", data)
|
||||||
}
|
}
|
||||||
|
|
||||||
c.HTML(200, "index")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NotFoundHandler(c *middleware.Context) {
|
func NotFoundHandler(c *middleware.Context) {
|
||||||
@ -70,10 +125,10 @@ func NotFoundHandler(c *middleware.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := setIndexViewData(c); err != nil {
|
if data, err := setIndexViewData(c); err != nil {
|
||||||
c.Handle(500, "Failed to get settings", err)
|
c.Handle(500, "Failed to get settings", err)
|
||||||
return
|
return
|
||||||
|
} else {
|
||||||
|
c.HTML(404, "index", data)
|
||||||
}
|
}
|
||||||
|
|
||||||
c.HTML(404, "index")
|
|
||||||
}
|
}
|
||||||
|
@ -19,19 +19,19 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func LoginView(c *middleware.Context) {
|
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)
|
c.Handle(500, "Failed to get settings", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
settings := c.Data["Settings"].(map[string]interface{})
|
viewData.Settings["googleAuthEnabled"] = setting.OAuthService.Google
|
||||||
settings["googleAuthEnabled"] = setting.OAuthService.Google
|
viewData.Settings["githubAuthEnabled"] = setting.OAuthService.GitHub
|
||||||
settings["githubAuthEnabled"] = setting.OAuthService.GitHub
|
viewData.Settings["disableUserSignUp"] = !setting.AllowUserSignUp
|
||||||
settings["disableUserSignUp"] = !setting.AllowUserSignUp
|
viewData.Settings["loginHint"] = setting.LoginHint
|
||||||
settings["loginHint"] = setting.LoginHint
|
|
||||||
|
|
||||||
if !tryLoginUsingRememberCookie(c) {
|
if !tryLoginUsingRememberCookie(c) {
|
||||||
c.HTML(200, VIEW_INDEX)
|
c.HTML(200, VIEW_INDEX, viewData)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
65
pkg/api/plugin_bundle.go
Normal file
65
pkg/api/plugin_bundle.go
Normal file
@ -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")
|
||||||
|
}
|
@ -14,6 +14,7 @@ import (
|
|||||||
"github.com/grafana/grafana/pkg/api/static"
|
"github.com/grafana/grafana/pkg/api/static"
|
||||||
"github.com/grafana/grafana/pkg/log"
|
"github.com/grafana/grafana/pkg/log"
|
||||||
"github.com/grafana/grafana/pkg/middleware"
|
"github.com/grafana/grafana/pkg/middleware"
|
||||||
|
"github.com/grafana/grafana/pkg/plugins"
|
||||||
"github.com/grafana/grafana/pkg/setting"
|
"github.com/grafana/grafana/pkg/setting"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -28,12 +29,18 @@ func newMacaron() *macaron.Macaron {
|
|||||||
m.Use(middleware.Gziper())
|
m.Use(middleware.Gziper())
|
||||||
}
|
}
|
||||||
|
|
||||||
mapStatic(m, "", "public")
|
for _, route := range plugins.StaticRoutes {
|
||||||
mapStatic(m, "app", "app")
|
pluginRoute := path.Join("/public/plugins/", route.Url)
|
||||||
mapStatic(m, "css", "css")
|
log.Info("Plugin: Adding static route %s -> %s", pluginRoute, route.Path)
|
||||||
mapStatic(m, "img", "img")
|
mapStatic(m, route.Path, "", pluginRoute)
|
||||||
mapStatic(m, "fonts", "fonts")
|
}
|
||||||
mapStatic(m, "robots.txt", "robots.txt")
|
|
||||||
|
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{
|
m.Use(macaron.Renderer(macaron.RenderOptions{
|
||||||
Directory: path.Join(setting.StaticRootPath, "views"),
|
Directory: path.Join(setting.StaticRootPath, "views"),
|
||||||
@ -51,7 +58,7 @@ func newMacaron() *macaron.Macaron {
|
|||||||
return m
|
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) {
|
headers := func(c *macaron.Context) {
|
||||||
c.Resp.Header().Set("Cache-Control", "public, max-age=3600")
|
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(
|
m.Use(httpstatic.Static(
|
||||||
path.Join(setting.StaticRootPath, dir),
|
path.Join(rootDir, dir),
|
||||||
httpstatic.StaticOptions{
|
httpstatic.StaticOptions{
|
||||||
SkipLogging: true,
|
SkipLogging: true,
|
||||||
Prefix: prefix,
|
Prefix: prefix,
|
||||||
|
@ -131,8 +131,8 @@ func (a *ldapAuther) getGrafanaUserFor(ldapUser *ldapUserInfo) (*m.User, error)
|
|||||||
}
|
}
|
||||||
|
|
||||||
return userQuery.Result, nil
|
return userQuery.Result, nil
|
||||||
}
|
|
||||||
|
|
||||||
|
}
|
||||||
func (a *ldapAuther) createGrafanaUser(ldapUser *ldapUserInfo) (*m.User, error) {
|
func (a *ldapAuther) createGrafanaUser(ldapUser *ldapUserInfo) (*m.User, error) {
|
||||||
cmd := m.CreateUserCommand{
|
cmd := m.CreateUserCommand{
|
||||||
Login: ldapUser.Username,
|
Login: ldapUser.Username,
|
||||||
|
34
pkg/models/plugin_bundle.go
Normal file
34
pkg/models/plugin_bundle.go
Normal file
@ -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
pkg/plugins/models.go
Normal file
85
pkg/plugins/models.go
Normal file
@ -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),
|
||||||
|
}
|
||||||
|
}
|
@ -6,18 +6,19 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/log"
|
"github.com/grafana/grafana/pkg/log"
|
||||||
|
"github.com/grafana/grafana/pkg/models"
|
||||||
"github.com/grafana/grafana/pkg/setting"
|
"github.com/grafana/grafana/pkg/setting"
|
||||||
)
|
)
|
||||||
|
|
||||||
type PluginMeta struct {
|
|
||||||
Type string `json:"type"`
|
|
||||||
Name string `json:"name"`
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
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 {
|
type PluginScanner struct {
|
||||||
@ -25,13 +26,53 @@ type PluginScanner struct {
|
|||||||
errors []error
|
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"))
|
scan(path.Join(setting.StaticRootPath, "app/plugins"))
|
||||||
|
checkExternalPluginPaths()
|
||||||
|
checkDependencies()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
func scan(pluginDir string) error {
|
||||||
DataSources = make(map[string]interface{})
|
|
||||||
|
|
||||||
scanner := &PluginScanner{
|
scanner := &PluginScanner{
|
||||||
pluginPath: pluginDir,
|
pluginPath: pluginDir,
|
||||||
}
|
}
|
||||||
@ -47,7 +88,7 @@ func scan(pluginDir string) error {
|
|||||||
return nil
|
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 {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -57,17 +98,25 @@ func (scanner *PluginScanner) walker(path string, f os.FileInfo, err error) erro
|
|||||||
}
|
}
|
||||||
|
|
||||||
if f.Name() == "plugin.json" {
|
if f.Name() == "plugin.json" {
|
||||||
err := scanner.loadPluginJson(path)
|
err := scanner.loadPluginJson(currentPath)
|
||||||
if err != nil {
|
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)
|
scanner.errors = append(scanner.errors, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (scanner *PluginScanner) loadPluginJson(path string) error {
|
func addStaticRoot(staticRootConfig *StaticRootConfig, currentDir string) {
|
||||||
reader, err := os.Open(path)
|
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 {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -87,12 +136,95 @@ func (scanner *PluginScanner) loadPluginJson(path string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if pluginType == "datasource" {
|
if pluginType == "datasource" {
|
||||||
datasourceType, exists := pluginJson["type"]
|
p := DataSourcePlugin{}
|
||||||
if !exists {
|
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")
|
return errors.New("Did not find type property in plugin.json")
|
||||||
}
|
}
|
||||||
DataSources[datasourceType.(string)] = pluginJson
|
|
||||||
|
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")
|
||||||
|
}
|
||||||
|
Bundles[p.Type] = p
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
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
|
||||||
|
}
|
||||||
|
@ -4,14 +4,17 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/grafana/grafana/pkg/setting"
|
||||||
. "github.com/smartystreets/goconvey/convey"
|
. "github.com/smartystreets/goconvey/convey"
|
||||||
|
"gopkg.in/ini.v1"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestPluginScans(t *testing.T) {
|
func TestPluginScans(t *testing.T) {
|
||||||
|
|
||||||
Convey("When scaning for plugins", t, func() {
|
Convey("When scaning for plugins", t, func() {
|
||||||
path, _ := filepath.Abs("../../public/app/plugins")
|
setting.StaticRootPath, _ = filepath.Abs("../../public/")
|
||||||
err := scan(path)
|
setting.Cfg = ini.Empty()
|
||||||
|
err := Init()
|
||||||
|
|
||||||
So(err, ShouldBeNil)
|
So(err, ShouldBeNil)
|
||||||
So(len(DataSources), ShouldBeGreaterThan, 1)
|
So(len(DataSources), ShouldBeGreaterThan, 1)
|
||||||
|
@ -18,6 +18,7 @@ func AddMigrations(mg *Migrator) {
|
|||||||
addApiKeyMigrations(mg)
|
addApiKeyMigrations(mg)
|
||||||
addDashboardSnapshotMigrations(mg)
|
addDashboardSnapshotMigrations(mg)
|
||||||
addQuotaMigration(mg)
|
addQuotaMigration(mg)
|
||||||
|
addPluginBundleMigration(mg)
|
||||||
}
|
}
|
||||||
|
|
||||||
func addMigrationLogMigrations(mg *Migrator) {
|
func addMigrationLogMigrations(mg *Migrator) {
|
||||||
|
26
pkg/services/sqlstore/migrations/plugin_bundle.go
Normal file
26
pkg/services/sqlstore/migrations/plugin_bundle.go
Normal file
@ -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
pkg/services/sqlstore/plugin_bundle.go
Normal file
46
pkg/services/sqlstore/plugin_bundle.go
Normal file
@ -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
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
@ -287,13 +287,11 @@ func loadSpecifedConfigFile(configFile string) {
|
|||||||
|
|
||||||
defaultSec, err := Cfg.GetSection(section.Name())
|
defaultSec, err := Cfg.GetSection(section.Name())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error(3, "Unknown config section %s defined in %s", section.Name(), configFile)
|
defaultSec, _ = Cfg.NewSection(section.Name())
|
||||||
continue
|
|
||||||
}
|
}
|
||||||
defaultKey, err := defaultSec.GetKey(key.Name())
|
defaultKey, err := defaultSec.GetKey(key.Name())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error(3, "Unknown config key %s defined in section %s, in file %s", key.Name(), section.Name(), configFile)
|
defaultKey, _ = defaultSec.NewKey(key.Name(), key.Value())
|
||||||
continue
|
|
||||||
}
|
}
|
||||||
defaultKey.SetValue(key.Value())
|
defaultKey.SetValue(key.Value())
|
||||||
}
|
}
|
||||||
|
@ -2,6 +2,7 @@ define([
|
|||||||
'angular',
|
'angular',
|
||||||
'jquery',
|
'jquery',
|
||||||
'lodash',
|
'lodash',
|
||||||
|
'app/core/config',
|
||||||
'require',
|
'require',
|
||||||
'bootstrap',
|
'bootstrap',
|
||||||
'angular-route',
|
'angular-route',
|
||||||
@ -12,7 +13,7 @@ define([
|
|||||||
'bindonce',
|
'bindonce',
|
||||||
'app/core/core',
|
'app/core/core',
|
||||||
],
|
],
|
||||||
function (angular, $, _, appLevelRequire) {
|
function (angular, $, _, config, appLevelRequire) {
|
||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
var app = angular.module('grafana', []);
|
var app = angular.module('grafana', []);
|
||||||
@ -35,6 +36,8 @@ function (angular, $, _, appLevelRequire) {
|
|||||||
} else {
|
} else {
|
||||||
_.extend(module, register_fns);
|
_.extend(module, register_fns);
|
||||||
}
|
}
|
||||||
|
// push it into the apps dependencies
|
||||||
|
apps_deps.push(module.name);
|
||||||
return module;
|
return module;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -64,13 +67,15 @@ function (angular, $, _, appLevelRequire) {
|
|||||||
var module_name = 'grafana.'+type;
|
var module_name = 'grafana.'+type;
|
||||||
// create the module
|
// create the module
|
||||||
app.useModule(angular.module(module_name, []));
|
app.useModule(angular.module(module_name, []));
|
||||||
// push it into the apps dependencies
|
|
||||||
apps_deps.push(module_name);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
var preBootRequires = [
|
var preBootRequires = ['app/features/all'];
|
||||||
'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() {
|
app.boot = function() {
|
||||||
require(preBootRequires, function () {
|
require(preBootRequires, function () {
|
||||||
|
@ -6,6 +6,7 @@ function (Settings) {
|
|||||||
|
|
||||||
var bootData = window.grafanaBootData || { settings: {} };
|
var bootData = window.grafanaBootData || { settings: {} };
|
||||||
var options = bootData.settings;
|
var options = bootData.settings;
|
||||||
|
options.bootData = bootData;
|
||||||
|
|
||||||
return new Settings(options);
|
return new Settings(options);
|
||||||
|
|
||||||
|
@ -15,19 +15,13 @@ function (angular, _, $, coreModule, config) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
$scope.setupMainNav = function() {
|
$scope.setupMainNav = function() {
|
||||||
|
_.each(config.bootData.mainNavLinks, function(item) {
|
||||||
$scope.mainLinks.push({
|
$scope.mainLinks.push({
|
||||||
text: "Dashboards",
|
text: item.text,
|
||||||
icon: "fa fa-fw fa-th-large",
|
icon: item.icon,
|
||||||
href: $scope.getUrl("/"),
|
href: $scope.getUrl(item.href)
|
||||||
});
|
});
|
||||||
|
|
||||||
if (contextSrv.hasRole('Admin')) {
|
|
||||||
$scope.mainLinks.push({
|
|
||||||
text: "Data Sources",
|
|
||||||
icon: "fa fa-fw fa-database",
|
|
||||||
href: $scope.getUrl("/datasources"),
|
|
||||||
});
|
});
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
$scope.loadOrgs = function() {
|
$scope.loadOrgs = function() {
|
||||||
|
@ -131,6 +131,16 @@ define([
|
|||||||
templateUrl: 'app/partials/reset_password.html',
|
templateUrl: 'app/partials/reset_password.html',
|
||||||
controller : 'ResetPasswordCtrl',
|
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', {
|
.when('/global-alerts', {
|
||||||
templateUrl: 'app/features/dashboard/partials/globalAlerts.html',
|
templateUrl: 'app/features/dashboard/partials/globalAlerts.html',
|
||||||
})
|
})
|
||||||
|
@ -12,8 +12,8 @@ function (angular, _, coreModule, store, config) {
|
|||||||
var self = this;
|
var self = this;
|
||||||
|
|
||||||
function User() {
|
function User() {
|
||||||
if (window.grafanaBootData.user) {
|
if (config.bootData.user) {
|
||||||
_.extend(this, window.grafanaBootData.user);
|
_.extend(this, config.bootData.user);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -8,15 +8,8 @@ function (_) {
|
|||||||
var defaults = {
|
var defaults = {
|
||||||
datasources : {},
|
datasources : {},
|
||||||
window_title_prefix : 'Grafana - ',
|
window_title_prefix : 'Grafana - ',
|
||||||
panels : {
|
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' },
|
|
||||||
},
|
|
||||||
new_panel_title: 'Panel Title',
|
new_panel_title: 'Panel Title',
|
||||||
plugins: {},
|
|
||||||
playlist_timespan: "1m",
|
playlist_timespan: "1m",
|
||||||
unsaved_changes_warning: true,
|
unsaved_changes_warning: true,
|
||||||
appSubUrl: ""
|
appSubUrl: ""
|
||||||
|
@ -6,4 +6,8 @@ define([
|
|||||||
'./userInviteCtrl',
|
'./userInviteCtrl',
|
||||||
'./orgApiKeysCtrl',
|
'./orgApiKeysCtrl',
|
||||||
'./orgDetailsCtrl',
|
'./orgDetailsCtrl',
|
||||||
|
'./pluginsCtrl',
|
||||||
|
'./pluginEditCtrl',
|
||||||
|
'./plugin_srv',
|
||||||
|
'./plugin_directive',
|
||||||
], function () {});
|
], function () {});
|
||||||
|
3
public/app/features/org/partials/pluginConfigCore.html
Normal file
3
public/app/features/org/partials/pluginConfigCore.html
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
<div>
|
||||||
|
{{current.type}} plugin does not have any additional config.
|
||||||
|
</div>
|
42
public/app/features/org/partials/pluginEdit.html
Normal file
42
public/app/features/org/partials/pluginEdit.html
Normal file
@ -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
|
||||||
|
<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
public/app/features/org/partials/plugins.html
Normal file
41
public/app/features/org/partials/plugins.html
Normal file
@ -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>
|
||||||
|
{{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
|
||||||
|
<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
public/app/features/org/pluginEditCtrl.js
Normal file
35
public/app/features/org/pluginEditCtrl.js
Normal file
@ -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
public/app/features/org/plugin_directive.js
Normal file
47
public/app/features/org/plugin_directive.js
Normal file
@ -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
public/app/features/org/plugin_srv.js
Normal file
58
public/app/features/org/plugin_srv.js
Normal file
@ -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
public/app/features/org/pluginsCtrl.js
Normal file
33
public/app/features/org/pluginsCtrl.js
Normal file
@ -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();
|
||||||
|
|
||||||
|
});
|
||||||
|
});
|
@ -13,9 +13,9 @@ function (angular, $, config) {
|
|||||||
restrict: 'E',
|
restrict: 'E',
|
||||||
link: function(scope, elem, attr) {
|
link: function(scope, elem, attr) {
|
||||||
var getter = $parse(attr.type), panelType = getter(scope);
|
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));
|
var panelEl = angular.element(document.createElement('grafana-panel-' + panelType));
|
||||||
elem.append(panelEl);
|
elem.append(panelEl);
|
||||||
$compile(panelEl)(scope);
|
$compile(panelEl)(scope);
|
||||||
|
13
public/app/plugins/external/example/readme.md
vendored
Normal file
13
public/app/plugins/external/example/readme.md
vendored
Normal file
@ -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
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -14,7 +14,7 @@ function (angular, app, _, config, PanelMeta) {
|
|||||||
module.directive('grafanaPanelDashlist', function() {
|
module.directive('grafanaPanelDashlist', function() {
|
||||||
return {
|
return {
|
||||||
controller: 'DashListPanelCtrl',
|
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,
|
fullscreen: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
$scope.panelMeta.addEditorTab('Options', 'app/panels/dashlist/editor.html');
|
$scope.panelMeta.addEditorTab('Options', 'app/plugins/panels/dashlist/editor.html');
|
||||||
|
|
||||||
var defaults = {
|
var defaults = {
|
||||||
mode: 'starred',
|
mode: 'starred',
|
8
public/app/plugins/panels/dashlist/plugin.json
Normal file
8
public/app/plugins/panels/dashlist/plugin.json
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"pluginType": "panel",
|
||||||
|
|
||||||
|
"name": "Dashboard list",
|
||||||
|
"type": "dashlist",
|
||||||
|
|
||||||
|
"module": "app/plugins/panels/dashlist/module"
|
||||||
|
}
|
@ -45,7 +45,7 @@ function (angular, _, $) {
|
|||||||
popoverScope.series = seriesInfo;
|
popoverScope.series = seriesInfo;
|
||||||
popoverSrv.show({
|
popoverSrv.show({
|
||||||
element: el,
|
element: el,
|
||||||
templateUrl: 'app/panels/graph/legend.popover.html',
|
templateUrl: 'app/plugins/panels/graph/legend.popover.html',
|
||||||
scope: popoverScope
|
scope: popoverScope
|
||||||
});
|
});
|
||||||
}
|
}
|
@ -17,7 +17,7 @@ function (angular, _, moment, kbn, TimeSeries, PanelMeta) {
|
|||||||
module.directive('grafanaPanelGraph', function() {
|
module.directive('grafanaPanelGraph', function() {
|
||||||
return {
|
return {
|
||||||
controller: 'GraphCtrl',
|
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,
|
metricsEditor: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
$scope.panelMeta.addEditorTab('Axes & Grid', 'app/panels/graph/axisEditor.html');
|
$scope.panelMeta.addEditorTab('Axes & Grid', 'app/plugins/panels/graph/axisEditor.html');
|
||||||
$scope.panelMeta.addEditorTab('Display Styles', 'app/panels/graph/styleEditor.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.addEditorTab('Time range', 'app/features/panel/partials/panelTime.html');
|
||||||
|
|
||||||
$scope.panelMeta.addExtendedMenuItem('Export CSV', '', 'exportCsv()');
|
$scope.panelMeta.addExtendedMenuItem('Export CSV', '', 'exportCsv()');
|
8
public/app/plugins/panels/graph/plugin.json
Normal file
8
public/app/plugins/panels/graph/plugin.json
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"pluginType": "panel",
|
||||||
|
|
||||||
|
"name": "Graph",
|
||||||
|
"type": "graph",
|
||||||
|
|
||||||
|
"module": "app/plugins/panels/graph/module"
|
||||||
|
}
|
@ -16,7 +16,7 @@ function (angular, app, _, kbn, TimeSeries, PanelMeta) {
|
|||||||
module.directive('grafanaPanelSinglestat', function() {
|
module.directive('grafanaPanelSinglestat', function() {
|
||||||
return {
|
return {
|
||||||
controller: 'SingleStatCtrl',
|
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.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');
|
$scope.panelMeta.addEditorTab('Time range', 'app/features/panel/partials/panelTime.html');
|
||||||
|
|
||||||
// Set and populate defaults
|
// Set and populate defaults
|
8
public/app/plugins/panels/singlestat/plugin.json
Normal file
8
public/app/plugins/panels/singlestat/plugin.json
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"pluginType": "panel",
|
||||||
|
|
||||||
|
"name": "Singlestat",
|
||||||
|
"type": "singlestat",
|
||||||
|
|
||||||
|
"module": "app/plugins/panels/singlestat/module"
|
||||||
|
}
|
@ -1,4 +1,4 @@
|
|||||||
///<reference path="../../headers/common.d.ts" />
|
///<reference path="../../../headers/common.d.ts" />
|
||||||
|
|
||||||
import angular = require('angular');
|
import angular = require('angular');
|
||||||
import _ = require('lodash');
|
import _ = require('lodash');
|
||||||
@ -21,7 +21,7 @@ export class TablePanelCtrl {
|
|||||||
metricsEditor: true,
|
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');
|
$scope.panelMeta.addEditorTab('Time range', 'app/features/panel/partials/panelTime.html');
|
||||||
|
|
||||||
var panelDefaults = {
|
var panelDefaults = {
|
@ -1,5 +1,4 @@
|
|||||||
///<reference path="../../headers/common.d.ts" />
|
///<reference path="../../../headers/common.d.ts" />
|
||||||
|
|
||||||
|
|
||||||
import angular = require('angular');
|
import angular = require('angular');
|
||||||
import $ = require('jquery');
|
import $ = require('jquery');
|
||||||
@ -122,4 +121,3 @@ export function tablePanelEditor($q, uiSegmentSrv) {
|
|||||||
controller: TablePanelEditorCtrl,
|
controller: TablePanelEditorCtrl,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
@ -1,4 +1,4 @@
|
|||||||
///<reference path="../../headers/common.d.ts" />
|
///<reference path="../../../headers/common.d.ts" />
|
||||||
|
|
||||||
import angular = require('angular');
|
import angular = require('angular');
|
||||||
import $ = require('jquery');
|
import $ = require('jquery');
|
||||||
@ -14,7 +14,7 @@ export function tablePanel() {
|
|||||||
'use strict';
|
'use strict';
|
||||||
return {
|
return {
|
||||||
restrict: 'E',
|
restrict: 'E',
|
||||||
templateUrl: 'app/panels/table/module.html',
|
templateUrl: 'app/plugins/panels/table/module.html',
|
||||||
controller: TablePanelCtrl,
|
controller: TablePanelCtrl,
|
||||||
link: function(scope, elem) {
|
link: function(scope, elem) {
|
||||||
var data;
|
var data;
|
8
public/app/plugins/panels/table/plugin.json
Normal file
8
public/app/plugins/panels/table/plugin.json
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"pluginType": "panel",
|
||||||
|
|
||||||
|
"name": "Table",
|
||||||
|
"type": "table",
|
||||||
|
|
||||||
|
"module": "app/plugins/panels/table/module"
|
||||||
|
}
|
@ -1,4 +1,4 @@
|
|||||||
///<reference path="../../headers/common.d.ts" />
|
///<reference path="../../../headers/common.d.ts" />
|
||||||
|
|
||||||
import _ = require('lodash');
|
import _ = require('lodash');
|
||||||
import kbn = require('app/core/utils/kbn');
|
import kbn = require('app/core/utils/kbn');
|
52
public/app/plugins/panels/table/table_model.ts
Normal file
52
public/app/plugins/panels/table/table_model.ts
Normal file
@ -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,4 +1,4 @@
|
|||||||
///<reference path="../../headers/common.d.ts" />
|
///<reference path="../../../headers/common.d.ts" />
|
||||||
|
|
||||||
import moment = require('moment');
|
import moment = require('moment');
|
||||||
import _ = require('lodash');
|
import _ = require('lodash');
|
@ -16,7 +16,7 @@ function (angular, app, _, require, PanelMeta) {
|
|||||||
module.directive('grafanaPanelText', function() {
|
module.directive('grafanaPanelText', function() {
|
||||||
return {
|
return {
|
||||||
controller: 'TextPanelCtrl',
|
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,
|
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
|
// Set and populate defaults
|
||||||
var _d = {
|
var _d = {
|
||||||
@ -84,7 +84,7 @@ function (angular, app, _, require, PanelMeta) {
|
|||||||
$scope.updateContent(converter.makeHtml(text));
|
$scope.updateContent(converter.makeHtml(text));
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
require(['./lib/showdown'], function (Showdown) {
|
require(['vendor/showdown'], function (Showdown) {
|
||||||
converter = new Showdown.converter();
|
converter = new Showdown.converter();
|
||||||
$scope.updateContent(converter.makeHtml(text));
|
$scope.updateContent(converter.makeHtml(text));
|
||||||
});
|
});
|
8
public/app/plugins/panels/text/plugin.json
Normal file
8
public/app/plugins/panels/text/plugin.json
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"pluginType": "panel",
|
||||||
|
|
||||||
|
"name": "Text",
|
||||||
|
"type": "text",
|
||||||
|
|
||||||
|
"module": "app/plugins/panels/text/module"
|
||||||
|
}
|
9
public/app/plugins/plugin.json
Normal file
9
public/app/plugins/plugin.json
Normal file
@ -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": []
|
||||||
|
}
|
@ -2,7 +2,7 @@ define([
|
|||||||
'./helpers',
|
'./helpers',
|
||||||
'app/features/panel/panel_srv',
|
'app/features/panel/panel_srv',
|
||||||
'app/features/panel/panel_helper',
|
'app/features/panel/panel_helper',
|
||||||
'app/panels/graph/module'
|
'app/plugins/panels/graph/module'
|
||||||
], function(helpers) {
|
], function(helpers) {
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
|
@ -3,7 +3,7 @@ define([
|
|||||||
'angular',
|
'angular',
|
||||||
'jquery',
|
'jquery',
|
||||||
'app/core/time_series',
|
'app/core/time_series',
|
||||||
'app/panels/graph/graph'
|
'app/plugins/panels/graph/graph'
|
||||||
], function(helpers, angular, $, TimeSeries) {
|
], function(helpers, angular, $, TimeSeries) {
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
define([
|
define([
|
||||||
'jquery',
|
'jquery',
|
||||||
'app/panels/graph/graph.tooltip'
|
'app/plugins/panels/graph/graph.tooltip'
|
||||||
], function($, GraphTooltip) {
|
], function($, GraphTooltip) {
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
define([
|
define([
|
||||||
'./helpers',
|
'./helpers',
|
||||||
'app/panels/graph/seriesOverridesCtrl'
|
'app/plugins/panels/graph/seriesOverridesCtrl'
|
||||||
], function(helpers) {
|
], function(helpers) {
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
|
@ -2,7 +2,7 @@ define([
|
|||||||
'./helpers',
|
'./helpers',
|
||||||
'app/features/panel/panel_srv',
|
'app/features/panel/panel_srv',
|
||||||
'app/features/panel/panel_helper',
|
'app/features/panel/panel_helper',
|
||||||
'app/panels/singlestat/module'
|
'app/plugins/panels/singlestat/module'
|
||||||
], function(helpers) {
|
], function(helpers) {
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
|
@ -95,6 +95,8 @@ function file2moduleName(filePath) {
|
|||||||
.replace(/\.\w*$/, '');
|
.replace(/\.\w*$/, '');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
window.grafanaBootData = {settings: {}};
|
||||||
|
|
||||||
require([
|
require([
|
||||||
'lodash',
|
'lodash',
|
||||||
'angular',
|
'angular',
|
||||||
|
@ -10,10 +10,18 @@
|
|||||||
|
|
||||||
[[if .User.LightTheme]]
|
[[if .User.LightTheme]]
|
||||||
<link rel="stylesheet" href="[[.AppSubUrl]]/public/css/grafana.light.min.css">
|
<link rel="stylesheet" href="[[.AppSubUrl]]/public/css/grafana.light.min.css">
|
||||||
|
[[ range $css := .PluginCss ]]
|
||||||
|
<link rel="stylesheet" href="[[$.AppSubUrl]]/[[ $css.Light ]]">
|
||||||
|
[[ end ]]
|
||||||
[[else]]
|
[[else]]
|
||||||
<link rel="stylesheet" href="[[.AppSubUrl]]/public/css/grafana.dark.min.css">
|
<link rel="stylesheet" href="[[.AppSubUrl]]/public/css/grafana.dark.min.css">
|
||||||
|
[[ range $css := .PluginCss ]]
|
||||||
|
<link rel="stylesheet" href="[[$.AppSubUrl]]/[[ $css.Dark ]]">
|
||||||
|
[[ end ]]
|
||||||
[[end]]
|
[[end]]
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<link rel="icon" type="image/png" href="[[.AppSubUrl]]/public/img/fav32.png">
|
<link rel="icon" type="image/png" href="[[.AppSubUrl]]/public/img/fav32.png">
|
||||||
<base href="[[.AppSubUrl]]/" />
|
<base href="[[.AppSubUrl]]/" />
|
||||||
|
|
||||||
@ -50,6 +58,8 @@
|
|||||||
window.grafanaBootData = {
|
window.grafanaBootData = {
|
||||||
user:[[.User]],
|
user:[[.User]],
|
||||||
settings: [[.Settings]],
|
settings: [[.Settings]],
|
||||||
|
pluginModules: [[.PluginJs]],
|
||||||
|
mainNavLinks: [[.MainNavLinks]]
|
||||||
};
|
};
|
||||||
|
|
||||||
require(['app/app'], function (app) {
|
require(['app/app'], function (app) {
|
||||||
|
@ -8,9 +8,7 @@ module.exports = function(config) {
|
|||||||
expand: true,
|
expand: true,
|
||||||
cwd: '<%= genDir %>',
|
cwd: '<%= genDir %>',
|
||||||
src: [
|
src: [
|
||||||
//'index.html',
|
'app/**/*.html',
|
||||||
'app/panels/**/*.html',
|
|
||||||
'app/partials/**/*.html'
|
|
||||||
],
|
],
|
||||||
dest: '<%= genDir %>'
|
dest: '<%= genDir %>'
|
||||||
}
|
}
|
||||||
|
@ -4,7 +4,6 @@ module.exports = function(config) {
|
|||||||
'Gruntfile.js',
|
'Gruntfile.js',
|
||||||
'<%= srcDir %>/app/**/*.js',
|
'<%= srcDir %>/app/**/*.js',
|
||||||
'<%= srcDir %>/plugins/**/*.js',
|
'<%= srcDir %>/plugins/**/*.js',
|
||||||
'!<%= srcDir %>/app/panels/*/{lib,leaflet}/*',
|
|
||||||
'!<%= srcDir %>/app/dashboards/*'
|
'!<%= srcDir %>/app/dashboards/*'
|
||||||
],
|
],
|
||||||
options: {
|
options: {
|
||||||
|
@ -18,7 +18,6 @@ module.exports = function(config) {
|
|||||||
'dist/*',
|
'dist/*',
|
||||||
'sample/*',
|
'sample/*',
|
||||||
'<%= srcDir %>/vendor/*',
|
'<%= srcDir %>/vendor/*',
|
||||||
'<%= srcDir %>/app/panels/*/{lib,leaflet}/*',
|
|
||||||
'<%= srcDir %>/app/dashboards/*'
|
'<%= srcDir %>/app/dashboards/*'
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@ -62,11 +62,11 @@ module.exports = function(config,grunt) {
|
|||||||
];
|
];
|
||||||
|
|
||||||
var fs = require('fs');
|
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/
|
// create a module for each directory in public/app/panels/
|
||||||
fs.readdirSync(panelPath).forEach(function (panelName) {
|
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 };
|
return { options: options };
|
||||||
|
Loading…
Reference in New Issue
Block a user