Merge branch 'master' into style-changes-bulletfactory
Conflicts: pkg/api/index.go public/less/sidemenu.less
This commit is contained in:
+10
-80
@@ -1,17 +1,9 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
"net/url"
|
||||
"text/template"
|
||||
|
||||
"gopkg.in/macaron.v1"
|
||||
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
"github.com/grafana/grafana/pkg/api/pluginproxy"
|
||||
"github.com/grafana/grafana/pkg/log"
|
||||
"github.com/grafana/grafana/pkg/middleware"
|
||||
m "github.com/grafana/grafana/pkg/models"
|
||||
@@ -22,16 +14,14 @@ import (
|
||||
func InitAppPluginRoutes(r *macaron.Macaron) {
|
||||
for _, plugin := range plugins.Apps {
|
||||
for _, route := range plugin.Routes {
|
||||
log.Info("Plugin: Adding proxy route for app plugin")
|
||||
url := util.JoinUrlFragments("/api/plugin-proxy/", route.Path)
|
||||
url := util.JoinUrlFragments("/api/plugin-proxy/"+plugin.Id, 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 != "" {
|
||||
handlers = append(handlers, middleware.Auth(&middleware.AuthOptions{
|
||||
ReqSignedIn: true,
|
||||
ReqGrafanaAdmin: route.ReqGrafanaAdmin,
|
||||
}))
|
||||
|
||||
if route.ReqRole != "" {
|
||||
if route.ReqRole == m.ROLE_ADMIN {
|
||||
handlers = append(handlers, middleware.RoleAuth(m.ROLE_ADMIN))
|
||||
} else if route.ReqRole == m.ROLE_EDITOR {
|
||||
@@ -40,7 +30,7 @@ func InitAppPluginRoutes(r *macaron.Macaron) {
|
||||
}
|
||||
handlers = append(handlers, AppPluginRoute(route, plugin.Id))
|
||||
r.Route(url, route.Method, handlers...)
|
||||
log.Info("Plugin: Adding route %s", url)
|
||||
log.Info("Plugins: Adding proxy route %s", url)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -49,68 +39,8 @@ func AppPluginRoute(route *plugins.AppPluginRoute, appId string) macaron.Handler
|
||||
return func(c *middleware.Context) {
|
||||
path := c.Params("*")
|
||||
|
||||
proxy := NewApiPluginProxy(c, path, route, appId)
|
||||
proxy := pluginproxy.NewApiPluginProxy(c, path, route, appId)
|
||||
proxy.Transport = dataProxyTransport
|
||||
proxy.ServeHTTP(c.Resp, c.Req.Request)
|
||||
}
|
||||
}
|
||||
|
||||
func NewApiPluginProxy(ctx *middleware.Context, proxyPath string, route *plugins.AppPluginRoute, appId string) *httputil.ReverseProxy {
|
||||
targetUrl, _ := url.Parse(route.Url)
|
||||
|
||||
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")
|
||||
|
||||
//Create a HTTP header with the context in it.
|
||||
ctxJson, err := json.Marshal(ctx.SignedInUser)
|
||||
if err != nil {
|
||||
ctx.JsonApiErr(500, "failed to marshal context to json.", err)
|
||||
return
|
||||
}
|
||||
|
||||
req.Header.Add("Grafana-Context", string(ctxJson))
|
||||
// add custom headers defined in the plugin config.
|
||||
for _, header := range route.Headers {
|
||||
var contentBuf bytes.Buffer
|
||||
t, err := template.New("content").Parse(header.Content)
|
||||
if err != nil {
|
||||
ctx.JsonApiErr(500, fmt.Sprintf("could not parse header content template for header %s.", header.Name), err)
|
||||
return
|
||||
}
|
||||
|
||||
//lookup appSettings
|
||||
query := m.GetAppSettingByAppIdQuery{OrgId: ctx.OrgId, AppId: appId}
|
||||
|
||||
if err := bus.Dispatch(&query); err != nil {
|
||||
ctx.JsonApiErr(500, "failed to get AppSettings.", err)
|
||||
return
|
||||
}
|
||||
type templateData struct {
|
||||
JsonData map[string]interface{}
|
||||
SecureJsonData map[string]string
|
||||
}
|
||||
data := templateData{
|
||||
JsonData: query.Result.JsonData,
|
||||
SecureJsonData: query.Result.SecureJsonData.Decrypt(),
|
||||
}
|
||||
err = t.Execute(&contentBuf, data)
|
||||
if err != nil {
|
||||
ctx.JsonApiErr(500, fmt.Sprintf("failed to execute header content template for header %s.", header.Name), err)
|
||||
return
|
||||
}
|
||||
log.Debug("Adding header to proxy request. %s: %s", header.Name, contentBuf.String())
|
||||
req.Header.Add(header.Name, contentBuf.String())
|
||||
}
|
||||
}
|
||||
|
||||
return &httputil.ReverseProxy{Director: director}
|
||||
}
|
||||
|
||||
+11
-9
@@ -6,15 +6,16 @@ import (
|
||||
)
|
||||
|
||||
type AppSettings struct {
|
||||
Name string `json:"name"`
|
||||
AppId string `json:"appId"`
|
||||
Enabled bool `json:"enabled"`
|
||||
Pinned bool `json:"pinned"`
|
||||
Module string `json:"module"`
|
||||
Info *plugins.PluginInfo `json:"info"`
|
||||
Pages []plugins.AppPluginPage `json:"pages"`
|
||||
Includes []plugins.AppIncludeInfo `json:"includes"`
|
||||
JsonData map[string]interface{} `json:"jsonData"`
|
||||
Name string `json:"name"`
|
||||
AppId string `json:"appId"`
|
||||
Enabled bool `json:"enabled"`
|
||||
Pinned bool `json:"pinned"`
|
||||
Module string `json:"module"`
|
||||
BaseUrl string `json:"baseUrl"`
|
||||
Info *plugins.PluginInfo `json:"info"`
|
||||
Pages []*plugins.AppPluginPage `json:"pages"`
|
||||
Includes []*plugins.AppIncludeInfo `json:"includes"`
|
||||
JsonData map[string]interface{} `json:"jsonData"`
|
||||
}
|
||||
|
||||
func NewAppSettingsDto(def *plugins.AppPlugin, data *models.AppSettings) *AppSettings {
|
||||
@@ -23,6 +24,7 @@ func NewAppSettingsDto(def *plugins.AppPlugin, data *models.AppSettings) *AppSet
|
||||
Name: def.Name,
|
||||
Info: &def.Info,
|
||||
Module: def.Module,
|
||||
BaseUrl: def.BaseUrl,
|
||||
Pages: def.Pages,
|
||||
Includes: def.Includes,
|
||||
}
|
||||
|
||||
@@ -7,10 +7,7 @@ type IndexViewData struct {
|
||||
AppSubUrl string
|
||||
GoogleAnalyticsId string
|
||||
GoogleTagManagerId string
|
||||
|
||||
PluginCss []*PluginCss
|
||||
PluginModules []string
|
||||
MainNavLinks []*NavLink
|
||||
MainNavLinks []*NavLink
|
||||
}
|
||||
|
||||
type PluginCss struct {
|
||||
@@ -19,8 +16,9 @@ type PluginCss struct {
|
||||
}
|
||||
|
||||
type NavLink struct {
|
||||
Text string `json:"text"`
|
||||
Icon string `json:"icon"`
|
||||
Img string `json:"img"`
|
||||
Url string `json:"url"`
|
||||
Text string `json:"text"`
|
||||
Icon string `json:"icon"`
|
||||
Img string `json:"img"`
|
||||
Url string `json:"url"`
|
||||
Children []*NavLink `json:"children"`
|
||||
}
|
||||
|
||||
@@ -121,10 +121,11 @@ func getFrontendSettingsMap(c *middleware.Context) (map[string]interface{}, erro
|
||||
panels := map[string]interface{}{}
|
||||
for _, panel := range enabledPlugins.Panels {
|
||||
panels[panel.Id] = map[string]interface{}{
|
||||
"module": panel.Module,
|
||||
"name": panel.Name,
|
||||
"id": panel.Id,
|
||||
"info": panel.Info,
|
||||
"module": panel.Module,
|
||||
"baseUrl": panel.BaseUrl,
|
||||
"name": panel.Name,
|
||||
"id": panel.Id,
|
||||
"info": panel.Info,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+23
-15
@@ -36,7 +36,7 @@ func setIndexViewData(c *middleware.Context) (*dtos.IndexViewData, error) {
|
||||
}
|
||||
|
||||
if setting.DisableGravatar {
|
||||
data.User.GravatarUrl = setting.AppSubUrl + "/img/user_profile.png"
|
||||
data.User.GravatarUrl = setting.AppSubUrl + "/public/img/user_profile.png"
|
||||
}
|
||||
|
||||
if len(data.User.Name) == 0 {
|
||||
@@ -51,20 +51,27 @@ func setIndexViewData(c *middleware.Context) (*dtos.IndexViewData, error) {
|
||||
data.MainNavLinks = append(data.MainNavLinks, &dtos.NavLink{
|
||||
Text: "Dashboards",
|
||||
Icon: "icon-gf icon-gf-dashboard",
|
||||
Url: "/",
|
||||
Url: setting.AppSubUrl + "/",
|
||||
Children: []*dtos.NavLink{
|
||||
{Text: "Playlists", Icon: "fa fa-fw fa-list", Url: setting.AppSubUrl + "/playlists"},
|
||||
{Text: "Snapshots", Icon: "fa-fw icon-gf icon-gf-snapshot", Url: setting.AppSubUrl + "/dashboard/snapshots"},
|
||||
},
|
||||
})
|
||||
|
||||
// data.MainNavLinks = append(data.MainNavLinks, &dtos.NavLink{Text: "Playlists", Icon: "fa fa-fw fa-list", Url: setting.AppSubUrl + "/playlists"})
|
||||
// data.MainNavLinks = append(data.MainNavLinks, &dtos.NavLink{Text: "Snapshots", Icon: "fa-fw icon-gf icon-gf-snapshot", Url: setting.AppSubUrl + "/dashboard/snapshots"})
|
||||
|
||||
if c.OrgRole == m.ROLE_ADMIN {
|
||||
data.MainNavLinks = append(data.MainNavLinks, &dtos.NavLink{
|
||||
Text: "Data Sources",
|
||||
Icon: "icon-gf icon-gf-datasources",
|
||||
Url: "/datasources",
|
||||
Url: setting.AppSubUrl + "/datasources",
|
||||
})
|
||||
|
||||
data.MainNavLinks = append(data.MainNavLinks, &dtos.NavLink{
|
||||
Text: "Plugins",
|
||||
Icon: "icon-gf icon-gf-apps",
|
||||
Url: "/apps",
|
||||
Url: setting.AppSubUrl + "/apps",
|
||||
})
|
||||
}
|
||||
|
||||
@@ -74,20 +81,21 @@ func setIndexViewData(c *middleware.Context) (*dtos.IndexViewData, error) {
|
||||
}
|
||||
|
||||
for _, plugin := range enabledPlugins.Apps {
|
||||
if plugin.Module != "" {
|
||||
data.PluginModules = append(data.PluginModules, plugin.Module)
|
||||
}
|
||||
|
||||
if plugin.Css != nil {
|
||||
data.PluginCss = append(data.PluginCss, &dtos.PluginCss{Light: plugin.Css.Light, Dark: plugin.Css.Dark})
|
||||
}
|
||||
|
||||
if plugin.Pinned {
|
||||
data.MainNavLinks = append(data.MainNavLinks, &dtos.NavLink{
|
||||
pageLink := &dtos.NavLink{
|
||||
Text: plugin.Name,
|
||||
Url: "/apps/edit/" + plugin.Id,
|
||||
Url: setting.AppSubUrl + "/apps/" + plugin.Id + "/edit",
|
||||
Img: plugin.Info.Logos.Small,
|
||||
})
|
||||
}
|
||||
|
||||
for _, page := range plugin.Pages {
|
||||
pageLink.Children = append(pageLink.Children, &dtos.NavLink{
|
||||
Url: setting.AppSubUrl + "/apps/" + plugin.Id + "/page/" + page.Slug,
|
||||
Text: page.Name,
|
||||
})
|
||||
}
|
||||
|
||||
data.MainNavLinks = append(data.MainNavLinks, pageLink)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
package pluginproxy
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
"net/url"
|
||||
"text/template"
|
||||
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
"github.com/grafana/grafana/pkg/log"
|
||||
"github.com/grafana/grafana/pkg/middleware"
|
||||
m "github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/plugins"
|
||||
"github.com/grafana/grafana/pkg/util"
|
||||
)
|
||||
|
||||
type templateData struct {
|
||||
JsonData map[string]interface{}
|
||||
SecureJsonData map[string]string
|
||||
}
|
||||
|
||||
func getHeaders(route *plugins.AppPluginRoute, orgId int64, appId string) (http.Header, error) {
|
||||
result := http.Header{}
|
||||
|
||||
query := m.GetAppSettingByAppIdQuery{OrgId: orgId, AppId: appId}
|
||||
|
||||
if err := bus.Dispatch(&query); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
data := templateData{
|
||||
JsonData: query.Result.JsonData,
|
||||
SecureJsonData: query.Result.SecureJsonData.Decrypt(),
|
||||
}
|
||||
|
||||
for _, header := range route.Headers {
|
||||
var contentBuf bytes.Buffer
|
||||
t, err := template.New("content").Parse(header.Content)
|
||||
if err != nil {
|
||||
return nil, errors.New(fmt.Sprintf("could not parse header content template for header %s.", header.Name))
|
||||
}
|
||||
|
||||
err = t.Execute(&contentBuf, data)
|
||||
if err != nil {
|
||||
return nil, errors.New(fmt.Sprintf("failed to execute header content template for header %s.", header.Name))
|
||||
}
|
||||
|
||||
log.Trace("Adding header to proxy request. %s: %s", header.Name, contentBuf.String())
|
||||
result.Add(header.Name, contentBuf.String())
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func NewApiPluginProxy(ctx *middleware.Context, proxyPath string, route *plugins.AppPluginRoute, appId string) *httputil.ReverseProxy {
|
||||
targetUrl, _ := url.Parse(route.Url)
|
||||
|
||||
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")
|
||||
|
||||
//Create a HTTP header with the context in it.
|
||||
ctxJson, err := json.Marshal(ctx.SignedInUser)
|
||||
if err != nil {
|
||||
ctx.JsonApiErr(500, "failed to marshal context to json.", err)
|
||||
return
|
||||
}
|
||||
|
||||
req.Header.Add("X-Grafana-Context", string(ctxJson))
|
||||
|
||||
if len(route.Headers) > 0 {
|
||||
headers, err := getHeaders(route, ctx.OrgId, appId)
|
||||
if err != nil {
|
||||
ctx.JsonApiErr(500, "Could not generate plugin route header", err)
|
||||
return
|
||||
}
|
||||
|
||||
for key, value := range headers {
|
||||
log.Info("setting key %v value %v", key, value[0])
|
||||
req.Header.Set(key, value[0])
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return &httputil.ReverseProxy{Director: director}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package pluginproxy
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
m "github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/plugins"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"github.com/grafana/grafana/pkg/util"
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
)
|
||||
|
||||
func TestPluginProxy(t *testing.T) {
|
||||
|
||||
Convey("When getting proxy headers", t, func() {
|
||||
route := &plugins.AppPluginRoute{
|
||||
Headers: []plugins.AppPluginRouteHeader{
|
||||
{Name: "x-header", Content: "my secret {{.SecureJsonData.key}}"},
|
||||
},
|
||||
}
|
||||
|
||||
setting.SecretKey = "password"
|
||||
|
||||
bus.AddHandler("test", func(query *m.GetAppSettingByAppIdQuery) error {
|
||||
query.Result = &m.AppSettings{
|
||||
SecureJsonData: map[string][]byte{
|
||||
"key": util.Encrypt([]byte("123"), "password"),
|
||||
},
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
header, err := getHeaders(route, 1, "my-app")
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
Convey("Should render header template", func() {
|
||||
So(header.Get("x-header"), ShouldEqual, "my secret 123")
|
||||
})
|
||||
})
|
||||
|
||||
}
|
||||
+1
-4
@@ -31,14 +31,11 @@ func newMacaron() *macaron.Macaron {
|
||||
|
||||
for _, route := range plugins.StaticRoutes {
|
||||
pluginRoute := path.Join("/public/plugins/", route.PluginId)
|
||||
log.Info("Plugin: Adding static route %s -> %s", pluginRoute, route.Directory)
|
||||
log.Info("Plugins: Adding route %s -> %s", pluginRoute, route.Directory)
|
||||
mapStatic(m, route.Directory, "", pluginRoute)
|
||||
}
|
||||
|
||||
mapStatic(m, setting.StaticRootPath, "", "public")
|
||||
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{
|
||||
|
||||
@@ -49,6 +49,14 @@ type UpdateAppSettingsCmd struct {
|
||||
OrgId int64 `json:"-"`
|
||||
}
|
||||
|
||||
func (cmd *UpdateAppSettingsCmd) GetEncryptedJsonData() SecureJsonData {
|
||||
encrypted := make(SecureJsonData)
|
||||
for key, data := range cmd.SecureJsonData {
|
||||
encrypted[key] = util.Encrypt([]byte(data), setting.SecretKey)
|
||||
}
|
||||
return encrypted
|
||||
}
|
||||
|
||||
// ---------------------
|
||||
// QUERIES
|
||||
type GetAppSettingsQuery struct {
|
||||
|
||||
+34
-15
@@ -4,13 +4,15 @@ import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
|
||||
"github.com/gosimple/slug"
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
)
|
||||
|
||||
type AppPluginPage struct {
|
||||
Name string `json:"name"`
|
||||
Url string `json:"url"`
|
||||
ReqRole models.RoleType `json:"reqRole"`
|
||||
Name string `json:"name"`
|
||||
Slug string `json:"slug"`
|
||||
Component string `json:"component"`
|
||||
Role models.RoleType `json:"role"`
|
||||
}
|
||||
|
||||
type AppPluginCss struct {
|
||||
@@ -26,10 +28,9 @@ type AppIncludeInfo struct {
|
||||
|
||||
type AppPlugin struct {
|
||||
FrontendPluginBase
|
||||
Css *AppPluginCss `json:"css"`
|
||||
Pages []AppPluginPage `json:"pages"`
|
||||
Pages []*AppPluginPage `json:"pages"`
|
||||
Routes []*AppPluginRoute `json:"routes"`
|
||||
Includes []AppIncludeInfo `json:"-"`
|
||||
Includes []*AppIncludeInfo `json:"-"`
|
||||
|
||||
Pinned bool `json:"-"`
|
||||
Enabled bool `json:"-"`
|
||||
@@ -38,7 +39,6 @@ type AppPlugin struct {
|
||||
type AppPluginRoute 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"`
|
||||
@@ -55,19 +55,22 @@ func (app *AppPlugin) Load(decoder *json.Decoder, pluginDir string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if app.Css != nil {
|
||||
app.Css.Dark = evalRelativePluginUrlPath(app.Css.Dark, app.Id)
|
||||
app.Css.Light = evalRelativePluginUrlPath(app.Css.Light, app.Id)
|
||||
if err := app.registerPlugin(pluginDir); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
app.PluginDir = pluginDir
|
||||
Apps[app.Id] = app
|
||||
return nil
|
||||
}
|
||||
|
||||
func (app *AppPlugin) initApp() {
|
||||
app.initFrontendPlugin()
|
||||
|
||||
// check if we have child panels
|
||||
for _, panel := range Panels {
|
||||
if strings.HasPrefix(panel.PluginDir, app.PluginDir) {
|
||||
panel.IncludedInAppId = app.Id
|
||||
app.Includes = append(app.Includes, AppIncludeInfo{
|
||||
panel.setPathsBasedOnApp(app)
|
||||
app.Includes = append(app.Includes, &AppIncludeInfo{
|
||||
Name: panel.Name,
|
||||
Id: panel.Id,
|
||||
Type: panel.Type,
|
||||
@@ -75,6 +78,22 @@ func (app *AppPlugin) Load(decoder *json.Decoder, pluginDir string) error {
|
||||
}
|
||||
}
|
||||
|
||||
Apps[app.Id] = app
|
||||
return nil
|
||||
// check if we have child datasources
|
||||
for _, ds := range DataSources {
|
||||
if strings.HasPrefix(ds.PluginDir, app.PluginDir) {
|
||||
ds.setPathsBasedOnApp(app)
|
||||
app.Includes = append(app.Includes, &AppIncludeInfo{
|
||||
Name: ds.Name,
|
||||
Id: ds.Id,
|
||||
Type: ds.Type,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// slugify pages
|
||||
for _, page := range app.Pages {
|
||||
if page.Slug == "" {
|
||||
page.Slug = slug.Make(page.Name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,9 +17,10 @@ func (p *DataSourcePlugin) Load(decoder *json.Decoder, pluginDir string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
p.PluginDir = pluginDir
|
||||
p.initFrontendPlugin()
|
||||
DataSources[p.Id] = p
|
||||
if err := p.registerPlugin(pluginDir); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
DataSources[p.Id] = p
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -4,11 +4,15 @@ import (
|
||||
"net/url"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/grafana/grafana/pkg/util"
|
||||
)
|
||||
|
||||
type FrontendPluginBase struct {
|
||||
PluginBase
|
||||
Module string `json:"module"`
|
||||
BaseUrl string `json:"baseUrl"`
|
||||
StaticRoot string `json:"staticRoot"`
|
||||
StaticRootAbs string `json:"-"`
|
||||
}
|
||||
@@ -22,22 +26,33 @@ func (fp *FrontendPluginBase) initFrontendPlugin() {
|
||||
})
|
||||
}
|
||||
|
||||
fp.handleModuleDefaults()
|
||||
|
||||
fp.Info.Logos.Small = evalRelativePluginUrlPath(fp.Info.Logos.Small, fp.Id)
|
||||
fp.Info.Logos.Large = evalRelativePluginUrlPath(fp.Info.Logos.Large, fp.Id)
|
||||
fp.handleModuleDefaults()
|
||||
|
||||
for i := 0; i < len(fp.Info.Screenshots); i++ {
|
||||
fp.Info.Screenshots[i].Path = evalRelativePluginUrlPath(fp.Info.Screenshots[i].Path, fp.Id)
|
||||
}
|
||||
}
|
||||
|
||||
func (fp *FrontendPluginBase) setPathsBasedOnApp(app *AppPlugin) {
|
||||
appSubPath := strings.Replace(fp.PluginDir, app.StaticRootAbs, "", 1)
|
||||
fp.IncludedInAppId = app.Id
|
||||
fp.BaseUrl = app.BaseUrl
|
||||
fp.Module = util.JoinUrlFragments("plugins/"+app.Id, appSubPath) + "/module"
|
||||
}
|
||||
|
||||
func (fp *FrontendPluginBase) handleModuleDefaults() {
|
||||
if fp.Module != "" {
|
||||
return
|
||||
}
|
||||
|
||||
if fp.StaticRoot != "" {
|
||||
fp.Module = path.Join("plugins", fp.Id, "module")
|
||||
fp.BaseUrl = path.Join("public/plugins", fp.Id)
|
||||
return
|
||||
}
|
||||
|
||||
fp.Module = path.Join("app/plugins", fp.Type, fp.Id, "module")
|
||||
fp.BaseUrl = path.Join("public/app/plugins", fp.Type, fp.Id)
|
||||
}
|
||||
|
||||
func evalRelativePluginUrlPath(pathStr string, pluginId string) string {
|
||||
|
||||
+31
-6
@@ -2,6 +2,11 @@ package plugins
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"strings"
|
||||
|
||||
"github.com/grafana/grafana/pkg/log"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
)
|
||||
|
||||
type PluginLoader interface {
|
||||
@@ -18,13 +23,28 @@ type PluginBase struct {
|
||||
PluginDir string `json:"-"`
|
||||
}
|
||||
|
||||
func (pb *PluginBase) registerPlugin(pluginDir string) error {
|
||||
if _, exists := Plugins[pb.Id]; exists {
|
||||
return errors.New("Plugin with same id already exists")
|
||||
}
|
||||
|
||||
if !strings.HasPrefix(pluginDir, setting.StaticRootPath) {
|
||||
log.Info("Plugins: Registering plugin %v", pb.Name)
|
||||
}
|
||||
|
||||
pb.PluginDir = pluginDir
|
||||
Plugins[pb.Id] = pb
|
||||
return nil
|
||||
}
|
||||
|
||||
type PluginInfo struct {
|
||||
Author PluginInfoLink `json:"author"`
|
||||
Description string `json:"description"`
|
||||
Links []PluginInfoLink `json:"links"`
|
||||
Logos PluginLogos `json:"logos"`
|
||||
Version string `json:"version"`
|
||||
Updated string `json:"updated"`
|
||||
Author PluginInfoLink `json:"author"`
|
||||
Description string `json:"description"`
|
||||
Links []PluginInfoLink `json:"links"`
|
||||
Logos PluginLogos `json:"logos"`
|
||||
Screenshots []PluginScreenshots `json:"screenshots"`
|
||||
Version string `json:"version"`
|
||||
Updated string `json:"updated"`
|
||||
}
|
||||
|
||||
type PluginInfoLink struct {
|
||||
@@ -37,6 +57,11 @@ type PluginLogos struct {
|
||||
Large string `json:"large"`
|
||||
}
|
||||
|
||||
type PluginScreenshots struct {
|
||||
Path string `json:"path"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
type PluginStaticRoute struct {
|
||||
Directory string
|
||||
PluginId string
|
||||
|
||||
@@ -11,9 +11,10 @@ func (p *PanelPlugin) Load(decoder *json.Decoder, pluginDir string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
p.PluginDir = pluginDir
|
||||
p.initFrontendPlugin()
|
||||
Panels[p.Id] = p
|
||||
if err := p.registerPlugin(pluginDir); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
Panels[p.Id] = p
|
||||
return nil
|
||||
}
|
||||
|
||||
+36
-24
@@ -19,6 +19,7 @@ var (
|
||||
Panels map[string]*PanelPlugin
|
||||
StaticRoutes []*PluginStaticRoute
|
||||
Apps map[string]*AppPlugin
|
||||
Plugins map[string]*PluginBase
|
||||
PluginTypes map[string]interface{}
|
||||
)
|
||||
|
||||
@@ -32,45 +33,50 @@ func Init() error {
|
||||
StaticRoutes = make([]*PluginStaticRoute, 0)
|
||||
Panels = make(map[string]*PanelPlugin)
|
||||
Apps = make(map[string]*AppPlugin)
|
||||
Plugins = make(map[string]*PluginBase)
|
||||
PluginTypes = map[string]interface{}{
|
||||
"panel": PanelPlugin{},
|
||||
"datasource": DataSourcePlugin{},
|
||||
"app": AppPlugin{},
|
||||
}
|
||||
|
||||
log.Info("Plugins: Scan starting")
|
||||
scan(path.Join(setting.StaticRootPath, "app/plugins"))
|
||||
scan(setting.PluginsPath)
|
||||
|
||||
// check if plugins dir exists
|
||||
if _, err := os.Stat(setting.PluginsPath); os.IsNotExist(err) {
|
||||
log.Warn("Plugins: Plugin dir %v does not exist", setting.PluginsPath)
|
||||
if err = os.MkdirAll(setting.PluginsPath, os.ModePerm); err != nil {
|
||||
log.Warn("Plugins: Failed to create plugin dir: %v, error: %v", setting.PluginsPath, err)
|
||||
} else {
|
||||
log.Info("Plugins: Plugin dir %v created", setting.PluginsPath)
|
||||
scan(setting.PluginsPath)
|
||||
}
|
||||
} else {
|
||||
scan(setting.PluginsPath)
|
||||
}
|
||||
|
||||
// check plugin paths defined in config
|
||||
checkPluginPaths()
|
||||
// checkDependencies()
|
||||
|
||||
for _, panel := range Panels {
|
||||
panel.initFrontendPlugin()
|
||||
}
|
||||
for _, panel := range DataSources {
|
||||
panel.initFrontendPlugin()
|
||||
}
|
||||
for _, app := range Apps {
|
||||
app.initApp()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// func checkDependencies() {
|
||||
// for appType, app := range Apps {
|
||||
// for _, reqPanel := range app.PanelPlugins {
|
||||
// if _, ok := Panels[reqPanel]; !ok {
|
||||
// log.Fatal(4, "App %s requires Panel type %s, but it is not present.", appType, reqPanel)
|
||||
// }
|
||||
// }
|
||||
// for _, reqDataSource := range app.DatasourcePlugins {
|
||||
// if _, ok := DataSources[reqDataSource]; !ok {
|
||||
// log.Fatal(4, "App %s requires DataSource type %s, but it is not present.", appType, reqDataSource)
|
||||
// }
|
||||
// }
|
||||
// for _, reqApiPlugin := range app.ApiPlugins {
|
||||
// if _, ok := ApiPlugins[reqApiPlugin]; !ok {
|
||||
// log.Fatal(4, "App %s requires ApiPlugin type %s, but it is not present.", appType, reqApiPlugin)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
func checkPluginPaths() error {
|
||||
for _, section := range setting.Cfg.Sections() {
|
||||
if strings.HasPrefix(section.Name(), "plugin.") {
|
||||
path := section.Key("path").String()
|
||||
if path != "" {
|
||||
log.Info("Plugin: Scaning dir %s", path)
|
||||
scan(path)
|
||||
}
|
||||
}
|
||||
@@ -83,6 +89,8 @@ func scan(pluginDir string) error {
|
||||
pluginPath: pluginDir,
|
||||
}
|
||||
|
||||
log.Info("Plugins: Scaning dir %s", pluginDir)
|
||||
|
||||
if err := util.Walk(pluginDir, true, true, scanner.walker); err != nil {
|
||||
if pluginDir != "data/plugins" {
|
||||
log.Warn("Could not scan dir \"%v\" error: %s", pluginDir, err)
|
||||
@@ -102,6 +110,10 @@ func (scanner *PluginScanner) walker(currentPath string, f os.FileInfo, err erro
|
||||
return err
|
||||
}
|
||||
|
||||
if f.Name() == "node_modules" {
|
||||
return util.WalkSkipDir
|
||||
}
|
||||
|
||||
if f.IsDir() {
|
||||
return nil
|
||||
}
|
||||
@@ -109,7 +121,7 @@ func (scanner *PluginScanner) walker(currentPath string, f os.FileInfo, err erro
|
||||
if f.Name() == "plugin.json" {
|
||||
err := scanner.loadPluginJson(currentPath)
|
||||
if err != nil {
|
||||
log.Error(3, "Failed to load plugin json file: %v, err: %v", currentPath, err)
|
||||
log.Error(3, "Plugins: Failed to load plugin json file: %v, err: %v", currentPath, err)
|
||||
scanner.errors = append(scanner.errors, err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,6 +34,7 @@ func TestPluginScans(t *testing.T) {
|
||||
So(err, ShouldBeNil)
|
||||
So(len(Apps), ShouldBeGreaterThan, 0)
|
||||
So(Apps["app-example"].Info.Logos.Large, ShouldEqual, "public/plugins/app-example/img/logo_large.png")
|
||||
So(Apps["app-example"].Info.Screenshots[1].Path, ShouldEqual, "public/plugins/app-example/img/screenshot2.png")
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
@@ -42,18 +42,13 @@ func UpdateAppSettings(cmd *m.UpdateAppSettingsCmd) error {
|
||||
sess.UseBool("enabled")
|
||||
sess.UseBool("pinned")
|
||||
if !exists {
|
||||
// encrypt secureJsonData
|
||||
secureJsonData := make(map[string][]byte)
|
||||
for key, data := range cmd.SecureJsonData {
|
||||
secureJsonData[key] = util.Encrypt([]byte(data), setting.SecretKey)
|
||||
}
|
||||
app = m.AppSettings{
|
||||
AppId: cmd.AppId,
|
||||
OrgId: cmd.OrgId,
|
||||
Enabled: cmd.Enabled,
|
||||
Pinned: cmd.Pinned,
|
||||
JsonData: cmd.JsonData,
|
||||
SecureJsonData: secureJsonData,
|
||||
SecureJsonData: cmd.GetEncryptedJsonData(),
|
||||
Created: time.Now(),
|
||||
Updated: time.Now(),
|
||||
}
|
||||
@@ -63,6 +58,7 @@ func UpdateAppSettings(cmd *m.UpdateAppSettingsCmd) error {
|
||||
for key, data := range cmd.SecureJsonData {
|
||||
app.SecureJsonData[key] = util.Encrypt([]byte(data), setting.SecretKey)
|
||||
}
|
||||
app.SecureJsonData = cmd.GetEncryptedJsonData()
|
||||
app.Updated = time.Now()
|
||||
app.Enabled = cmd.Enabled
|
||||
app.JsonData = cmd.JsonData
|
||||
|
||||
Reference in New Issue
Block a user