Merge branch 'apps'
This commit is contained in:
+9
-2
@@ -41,8 +41,8 @@ func Register(r *macaron.Macaron) {
|
||||
r.Get("/admin/orgs", reqGrafanaAdmin, Index)
|
||||
r.Get("/admin/orgs/edit/:id", reqGrafanaAdmin, Index)
|
||||
|
||||
r.Get("/plugins", reqSignedIn, Index)
|
||||
r.Get("/plugins/edit/*", reqSignedIn, Index)
|
||||
r.Get("/apps", reqSignedIn, Index)
|
||||
r.Get("/apps/edit/*", reqSignedIn, Index)
|
||||
|
||||
r.Get("/dashboard/*", reqSignedIn, Index)
|
||||
r.Get("/dashboard-solo/*", reqSignedIn, Index)
|
||||
@@ -120,6 +120,11 @@ func Register(r *macaron.Macaron) {
|
||||
r.Get("/invites", wrap(GetPendingOrgInvites))
|
||||
r.Post("/invites", quota("user"), bind(dtos.AddInviteForm{}), wrap(AddOrgInvite))
|
||||
r.Patch("/invites/:code/revoke", wrap(RevokeInvite))
|
||||
|
||||
// apps
|
||||
r.Get("/apps", wrap(GetOrgAppsList))
|
||||
r.Get("/apps/:appId/settings", wrap(GetAppSettingsById))
|
||||
r.Post("/apps/:appId/settings", bind(m.UpdateAppSettingsCmd{}), wrap(UpdateAppSettings))
|
||||
}, reqOrgAdmin)
|
||||
|
||||
// create new org
|
||||
@@ -205,5 +210,7 @@ func Register(r *macaron.Macaron) {
|
||||
// rendering
|
||||
r.Get("/render/*", reqSignedIn, RenderToPng)
|
||||
|
||||
InitApiPluginRoutes(r)
|
||||
|
||||
r.NotFound(NotFoundHandler)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
"net/url"
|
||||
|
||||
"github.com/Unknwon/macaron"
|
||||
"github.com/grafana/grafana/pkg/log"
|
||||
"github.com/grafana/grafana/pkg/middleware"
|
||||
m "github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/plugins"
|
||||
"github.com/grafana/grafana/pkg/util"
|
||||
)
|
||||
|
||||
func InitApiPluginRoutes(r *macaron.Macaron) {
|
||||
for _, plugin := range plugins.ApiPlugins {
|
||||
log.Info("Plugin: Adding proxy routes for api plugin")
|
||||
for _, route := range plugin.Routes {
|
||||
url := util.JoinUrlFragments("/api/plugin-proxy/", route.Path)
|
||||
handlers := make([]macaron.Handler, 0)
|
||||
if route.ReqSignedIn {
|
||||
handlers = append(handlers, middleware.Auth(&middleware.AuthOptions{ReqSignedIn: true}))
|
||||
}
|
||||
if route.ReqGrafanaAdmin {
|
||||
handlers = append(handlers, middleware.Auth(&middleware.AuthOptions{ReqSignedIn: true, ReqGrafanaAdmin: true}))
|
||||
}
|
||||
if route.ReqSignedIn && route.ReqRole != "" {
|
||||
if route.ReqRole == m.ROLE_ADMIN {
|
||||
handlers = append(handlers, middleware.RoleAuth(m.ROLE_ADMIN))
|
||||
} else if route.ReqRole == m.ROLE_EDITOR {
|
||||
handlers = append(handlers, middleware.RoleAuth(m.ROLE_EDITOR, m.ROLE_ADMIN))
|
||||
}
|
||||
}
|
||||
handlers = append(handlers, ApiPlugin(route.Url))
|
||||
r.Route(url, route.Method, handlers...)
|
||||
log.Info("Plugin: Adding route %s", url)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func ApiPlugin(routeUrl string) macaron.Handler {
|
||||
return func(c *middleware.Context) {
|
||||
path := c.Params("*")
|
||||
|
||||
//Create a HTTP header with the context in it.
|
||||
ctx, err := json.Marshal(c.SignedInUser)
|
||||
if err != nil {
|
||||
c.JsonApiErr(500, "failed to marshal context to json.", err)
|
||||
return
|
||||
}
|
||||
targetUrl, _ := url.Parse(routeUrl)
|
||||
proxy := NewApiPluginProxy(string(ctx), path, targetUrl)
|
||||
proxy.Transport = dataProxyTransport
|
||||
proxy.ServeHTTP(c.RW(), c.Req.Request)
|
||||
}
|
||||
}
|
||||
|
||||
func NewApiPluginProxy(ctx string, proxyPath string, targetUrl *url.URL) *httputil.ReverseProxy {
|
||||
director := func(req *http.Request) {
|
||||
req.URL.Scheme = targetUrl.Scheme
|
||||
req.URL.Host = targetUrl.Host
|
||||
req.Host = targetUrl.Host
|
||||
|
||||
req.URL.Path = util.JoinUrlFragments(targetUrl.Path, proxyPath)
|
||||
|
||||
// clear cookie headers
|
||||
req.Header.Del("Cookie")
|
||||
req.Header.Del("Set-Cookie")
|
||||
req.Header.Add("Grafana-Context", ctx)
|
||||
}
|
||||
|
||||
return &httputil.ReverseProxy{Director: director}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"github.com/grafana/grafana/pkg/api/dtos"
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
"github.com/grafana/grafana/pkg/middleware"
|
||||
m "github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/plugins"
|
||||
)
|
||||
|
||||
func GetOrgAppsList(c *middleware.Context) Response {
|
||||
orgApps, err := plugins.GetOrgAppSettings(c.OrgId)
|
||||
|
||||
if err != nil {
|
||||
return ApiError(500, "Failed to list of apps", err)
|
||||
}
|
||||
|
||||
result := make([]*dtos.AppSettings, 0)
|
||||
for _, app := range plugins.Apps {
|
||||
orgApp := orgApps[app.Id]
|
||||
result = append(result, dtos.NewAppSettingsDto(app, orgApp))
|
||||
}
|
||||
|
||||
return Json(200, result)
|
||||
}
|
||||
|
||||
func GetAppSettingsById(c *middleware.Context) Response {
|
||||
appId := c.Params(":appId")
|
||||
|
||||
if pluginDef, exists := plugins.Apps[appId]; !exists {
|
||||
return ApiError(404, "PluginId not found, no installed plugin with that id", nil)
|
||||
} else {
|
||||
orgApps, err := plugins.GetOrgAppSettings(c.OrgId)
|
||||
if err != nil {
|
||||
return ApiError(500, "Failed to get org app settings ", nil)
|
||||
}
|
||||
orgApp := orgApps[appId]
|
||||
|
||||
return Json(200, dtos.NewAppSettingsDto(pluginDef, orgApp))
|
||||
}
|
||||
}
|
||||
|
||||
func UpdateAppSettings(c *middleware.Context, cmd m.UpdateAppSettingsCmd) Response {
|
||||
appId := c.Params(":appId")
|
||||
|
||||
cmd.OrgId = c.OrgId
|
||||
cmd.AppId = appId
|
||||
|
||||
if _, ok := plugins.Apps[cmd.AppId]; !ok {
|
||||
return ApiError(404, "App type not installed.", nil)
|
||||
}
|
||||
|
||||
err := bus.Dispatch(&cmd)
|
||||
if err != nil {
|
||||
return ApiError(500, "Failed to update App Plugin", err)
|
||||
}
|
||||
|
||||
return ApiSuccess("App updated")
|
||||
}
|
||||
+13
-6
@@ -3,6 +3,7 @@ package api
|
||||
import (
|
||||
"github.com/grafana/grafana/pkg/api/dtos"
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
//"github.com/grafana/grafana/pkg/log"
|
||||
"github.com/grafana/grafana/pkg/middleware"
|
||||
m "github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/plugins"
|
||||
@@ -115,13 +116,19 @@ func UpdateDataSource(c *middleware.Context, cmd m.UpdateDataSourceCommand) {
|
||||
}
|
||||
|
||||
func GetDataSourcePlugins(c *middleware.Context) {
|
||||
dsList := make(map[string]interface{})
|
||||
dsList := make(map[string]*plugins.DataSourcePlugin)
|
||||
|
||||
for key, value := range plugins.DataSources {
|
||||
if !value.BuiltIn {
|
||||
dsList[key] = value
|
||||
if enabledPlugins, err := plugins.GetEnabledPlugins(c.OrgId); err != nil {
|
||||
c.JsonApiErr(500, "Failed to get org apps", err)
|
||||
return
|
||||
} else {
|
||||
|
||||
for key, value := range enabledPlugins.DataSources {
|
||||
if !value.BuiltIn {
|
||||
dsList[key] = value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
c.JSON(200, dsList)
|
||||
c.JSON(200, dsList)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
package dtos
|
||||
|
||||
import (
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/plugins"
|
||||
)
|
||||
|
||||
type AppSettings struct {
|
||||
Name string `json:"name"`
|
||||
AppId string `json:"appId"`
|
||||
Enabled bool `json:"enabled"`
|
||||
Pinned bool `json:"pinned"`
|
||||
Info *plugins.PluginInfo `json:"info"`
|
||||
Pages []*plugins.AppPluginPage `json:"pages"`
|
||||
JsonData map[string]interface{} `json:"jsonData"`
|
||||
}
|
||||
|
||||
func NewAppSettingsDto(def *plugins.AppPlugin, data *models.AppSettings) *AppSettings {
|
||||
dto := &AppSettings{
|
||||
AppId: def.Id,
|
||||
Name: def.Name,
|
||||
Info: &def.Info,
|
||||
Pages: def.Pages,
|
||||
}
|
||||
|
||||
if data != nil {
|
||||
dto.Enabled = data.Enabled
|
||||
dto.Pinned = data.Pinned
|
||||
dto.Info = &def.Info
|
||||
}
|
||||
|
||||
return dto
|
||||
}
|
||||
@@ -8,9 +8,9 @@ type IndexViewData struct {
|
||||
GoogleAnalyticsId string
|
||||
GoogleTagManagerId string
|
||||
|
||||
PluginCss []*PluginCss
|
||||
PluginJs []string
|
||||
MainNavLinks []*NavLink
|
||||
PluginCss []*PluginCss
|
||||
PluginModules []string
|
||||
MainNavLinks []*NavLink
|
||||
}
|
||||
|
||||
type PluginCss struct {
|
||||
@@ -21,5 +21,6 @@ type PluginCss struct {
|
||||
type NavLink struct {
|
||||
Text string `json:"text"`
|
||||
Icon string `json:"icon"`
|
||||
Href string `json:"href"`
|
||||
Img string `json:"img"`
|
||||
Url string `json:"url"`
|
||||
}
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
package dtos
|
||||
|
||||
type PluginBundle struct {
|
||||
Type string `json:"type"`
|
||||
Enabled bool `json:"enabled"`
|
||||
Module string `json:"module"`
|
||||
JsonData map[string]interface{} `json:"jsonData"`
|
||||
}
|
||||
@@ -29,6 +29,11 @@ func getFrontendSettingsMap(c *middleware.Context) (map[string]interface{}, erro
|
||||
datasources := make(map[string]interface{})
|
||||
var defaultDatasource string
|
||||
|
||||
enabledPlugins, err := plugins.GetEnabledPlugins(c.OrgId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, ds := range orgDataSources {
|
||||
url := ds.Url
|
||||
|
||||
@@ -42,7 +47,7 @@ func getFrontendSettingsMap(c *middleware.Context) (map[string]interface{}, erro
|
||||
"url": url,
|
||||
}
|
||||
|
||||
meta, exists := plugins.DataSources[ds.Type]
|
||||
meta, exists := enabledPlugins.DataSources[ds.Type]
|
||||
if !exists {
|
||||
log.Error(3, "Could not find plugin definition for data source: %v", ds.Type)
|
||||
continue
|
||||
@@ -110,8 +115,8 @@ func getFrontendSettingsMap(c *middleware.Context) (map[string]interface{}, erro
|
||||
}
|
||||
|
||||
panels := map[string]interface{}{}
|
||||
for _, panel := range plugins.Panels {
|
||||
panels[panel.Type] = map[string]interface{}{
|
||||
for _, panel := range enabledPlugins.Panels {
|
||||
panels[panel.Id] = map[string]interface{}{
|
||||
"module": panel.Module,
|
||||
"name": panel.Name,
|
||||
}
|
||||
|
||||
+32
-2
@@ -4,6 +4,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/api/dtos"
|
||||
"github.com/grafana/grafana/pkg/middleware"
|
||||
m "github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/plugins"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
)
|
||||
|
||||
@@ -50,7 +51,7 @@ func setIndexViewData(c *middleware.Context) (*dtos.IndexViewData, error) {
|
||||
data.MainNavLinks = append(data.MainNavLinks, &dtos.NavLink{
|
||||
Text: "Dashboards",
|
||||
Icon: "fa fa-fw fa-th-large",
|
||||
Href: "/",
|
||||
Url: "/",
|
||||
})
|
||||
|
||||
data.MainNavLinks = append(data.MainNavLinks, &dtos.NavLink{
|
||||
@@ -63,8 +64,37 @@ func setIndexViewData(c *middleware.Context) (*dtos.IndexViewData, error) {
|
||||
data.MainNavLinks = append(data.MainNavLinks, &dtos.NavLink{
|
||||
Text: "Data Sources",
|
||||
Icon: "fa fa-fw fa-database",
|
||||
Href: "/datasources",
|
||||
Url: "/datasources",
|
||||
})
|
||||
|
||||
data.MainNavLinks = append(data.MainNavLinks, &dtos.NavLink{
|
||||
Text: "Apps",
|
||||
Icon: "fa fa-fw fa-cubes",
|
||||
Url: "/apps",
|
||||
})
|
||||
}
|
||||
|
||||
enabledPlugins, err := plugins.GetEnabledPlugins(c.OrgId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, plugin := range enabledPlugins.Apps {
|
||||
if plugin.Module != "" {
|
||||
data.PluginModules = append(data.PluginModules, plugin.Module)
|
||||
}
|
||||
|
||||
if plugin.Css != nil {
|
||||
data.PluginCss = append(data.PluginCss, &dtos.PluginCss{Light: plugin.Css.Light, Dark: plugin.Css.Dark})
|
||||
}
|
||||
|
||||
if plugin.Pinned {
|
||||
data.MainNavLinks = append(data.MainNavLinks, &dtos.NavLink{
|
||||
Text: plugin.Name,
|
||||
Url: "/apps/edit/" + plugin.Id,
|
||||
Img: plugin.Info.Logos.Small,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return &data, nil
|
||||
|
||||
+3
-3
@@ -30,9 +30,9 @@ func newMacaron() *macaron.Macaron {
|
||||
}
|
||||
|
||||
for _, route := range plugins.StaticRoutes {
|
||||
pluginRoute := path.Join("/public/plugins/", route.Url)
|
||||
log.Info("Plugin: Adding static route %s -> %s", pluginRoute, route.Path)
|
||||
mapStatic(m, route.Path, "", pluginRoute)
|
||||
pluginRoute := path.Join("/public/plugins/", route.PluginId)
|
||||
log.Info("Plugin: Adding static route %s -> %s", pluginRoute, route.Directory)
|
||||
mapStatic(m, route.Directory, "", pluginRoute)
|
||||
}
|
||||
|
||||
mapStatic(m, setting.StaticRootPath, "", "public")
|
||||
|
||||
@@ -253,3 +253,7 @@ func (ctx *Context) JsonApiErr(status int, message string, err error) {
|
||||
|
||||
ctx.JSON(status, resp)
|
||||
}
|
||||
|
||||
func (ctx *Context) HasUserRole(role m.RoleType) bool {
|
||||
return ctx.OrgRole.Includes(role)
|
||||
}
|
||||
|
||||
@@ -2,11 +2,12 @@ package models
|
||||
|
||||
import "time"
|
||||
|
||||
type PluginBundle struct {
|
||||
type AppSettings struct {
|
||||
Id int64
|
||||
Type string
|
||||
AppId string
|
||||
OrgId int64
|
||||
Enabled bool
|
||||
Pinned bool
|
||||
JsonData map[string]interface{}
|
||||
|
||||
Created time.Time
|
||||
@@ -17,18 +18,18 @@ type PluginBundle struct {
|
||||
// COMMANDS
|
||||
|
||||
// Also acts as api DTO
|
||||
type UpdatePluginBundleCmd struct {
|
||||
Type string `json:"type" binding:"Required"`
|
||||
type UpdateAppSettingsCmd struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
Pinned bool `json:"pinned"`
|
||||
JsonData map[string]interface{} `json:"jsonData"`
|
||||
|
||||
Id int64 `json:"-"`
|
||||
OrgId int64 `json:"-"`
|
||||
AppId string `json:"-"`
|
||||
OrgId int64 `json:"-"`
|
||||
}
|
||||
|
||||
// ---------------------
|
||||
// QUERIES
|
||||
type GetPluginBundlesQuery struct {
|
||||
type GetAppSettingsQuery struct {
|
||||
OrgId int64
|
||||
Result []*PluginBundle
|
||||
Result []*AppSettings
|
||||
}
|
||||
@@ -26,6 +26,17 @@ func (r RoleType) IsValid() bool {
|
||||
return r == ROLE_VIEWER || r == ROLE_ADMIN || r == ROLE_EDITOR || r == ROLE_READ_ONLY_EDITOR
|
||||
}
|
||||
|
||||
func (r RoleType) Includes(other RoleType) bool {
|
||||
if r == ROLE_ADMIN {
|
||||
return true
|
||||
}
|
||||
if r == ROLE_EDITOR || r == ROLE_READ_ONLY_EDITOR {
|
||||
return other != ROLE_ADMIN
|
||||
}
|
||||
|
||||
return r == other
|
||||
}
|
||||
|
||||
type OrgUser struct {
|
||||
Id int64
|
||||
OrgId int64
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
package plugins
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
)
|
||||
|
||||
type AppPluginPage struct {
|
||||
Name string `json:"name"`
|
||||
Url string `json:"url"`
|
||||
ReqRole models.RoleType `json:"reqRole"`
|
||||
}
|
||||
|
||||
type AppPluginCss struct {
|
||||
Light string `json:"light"`
|
||||
Dark string `json:"dark"`
|
||||
}
|
||||
|
||||
type AppPlugin struct {
|
||||
FrontendPluginBase
|
||||
Css *AppPluginCss `json:"css"`
|
||||
Pages []*AppPluginPage `json:"pages"`
|
||||
|
||||
Pinned bool `json:"-"`
|
||||
Enabled bool `json:"-"`
|
||||
}
|
||||
|
||||
func (app *AppPlugin) Load(decoder *json.Decoder, pluginDir string) error {
|
||||
if err := decoder.Decode(&app); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if app.Css != nil {
|
||||
app.Css.Dark = evalRelativePluginUrlPath(app.Css.Dark, app.Id)
|
||||
app.Css.Light = evalRelativePluginUrlPath(app.Css.Light, app.Id)
|
||||
}
|
||||
|
||||
app.PluginDir = pluginDir
|
||||
app.initFrontendPlugin()
|
||||
Apps[app.Id] = app
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package plugins
|
||||
|
||||
import "encoding/json"
|
||||
|
||||
type DataSourcePlugin struct {
|
||||
FrontendPluginBase
|
||||
DefaultMatchFormat string `json:"defaultMatchFormat"`
|
||||
Annotations bool `json:"annotations"`
|
||||
Metrics bool `json:"metrics"`
|
||||
BuiltIn bool `json:"builtIn"`
|
||||
Mixed bool `json:"mixed"`
|
||||
App string `json:"app"`
|
||||
}
|
||||
|
||||
func (p *DataSourcePlugin) Load(decoder *json.Decoder, pluginDir string) error {
|
||||
if err := decoder.Decode(&p); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
p.PluginDir = pluginDir
|
||||
p.initFrontendPlugin()
|
||||
DataSources[p.Id] = p
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package plugins
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"path"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
type FrontendPluginBase struct {
|
||||
PluginBase
|
||||
Module string `json:"module"`
|
||||
StaticRoot string `json:"staticRoot"`
|
||||
}
|
||||
|
||||
func (fp *FrontendPluginBase) initFrontendPlugin() {
|
||||
if fp.StaticRoot != "" {
|
||||
StaticRoutes = append(StaticRoutes, &PluginStaticRoute{
|
||||
Directory: filepath.Join(fp.PluginDir, fp.StaticRoot),
|
||||
PluginId: fp.Id,
|
||||
})
|
||||
}
|
||||
|
||||
fp.Info.Logos.Small = evalRelativePluginUrlPath(fp.Info.Logos.Small, fp.Id)
|
||||
fp.Info.Logos.Large = evalRelativePluginUrlPath(fp.Info.Logos.Large, fp.Id)
|
||||
|
||||
fp.handleModuleDefaults()
|
||||
}
|
||||
|
||||
func (fp *FrontendPluginBase) handleModuleDefaults() {
|
||||
if fp.Module != "" {
|
||||
return
|
||||
}
|
||||
|
||||
if fp.StaticRoot != "" {
|
||||
fp.Module = path.Join("plugins", fp.Id, "module")
|
||||
return
|
||||
}
|
||||
|
||||
fp.Module = path.Join("app/plugins", fp.Type, fp.Id, "module")
|
||||
}
|
||||
|
||||
func evalRelativePluginUrlPath(pathStr string, pluginId string) string {
|
||||
u, _ := url.Parse(pathStr)
|
||||
if u.IsAbs() {
|
||||
return pathStr
|
||||
}
|
||||
return path.Join("public/plugins", pluginId, pathStr)
|
||||
}
|
||||
+66
-18
@@ -1,26 +1,74 @@
|
||||
package plugins
|
||||
|
||||
type DataSourcePlugin struct {
|
||||
Type string `json:"type"`
|
||||
Name string `json:"name"`
|
||||
ServiceName string `json:"serviceName"`
|
||||
Module string `json:"module"`
|
||||
Partials map[string]interface{} `json:"partials"`
|
||||
DefaultMatchFormat string `json:"defaultMatchFormat"`
|
||||
Annotations bool `json:"annotations"`
|
||||
Metrics bool `json:"metrics"`
|
||||
BuiltIn bool `json:"builtIn"`
|
||||
StaticRootConfig *StaticRootConfig `json:"staticRoot"`
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
)
|
||||
|
||||
type PluginLoader interface {
|
||||
Load(decoder *json.Decoder, pluginDir string) error
|
||||
}
|
||||
|
||||
type PanelPlugin struct {
|
||||
Type string `json:"type"`
|
||||
Name string `json:"name"`
|
||||
Module string `json:"module"`
|
||||
StaticRootConfig *StaticRootConfig `json:"staticRoot"`
|
||||
type PluginBase struct {
|
||||
Type string `json:"type"`
|
||||
Name string `json:"name"`
|
||||
Id string `json:"id"`
|
||||
App string `json:"app"`
|
||||
Info PluginInfo `json:"info"`
|
||||
PluginDir string `json:"-"`
|
||||
}
|
||||
|
||||
type StaticRootConfig struct {
|
||||
type PluginInfo struct {
|
||||
Author PluginInfoLink `json:"author"`
|
||||
Description string `json:"description"`
|
||||
Links []PluginInfoLink `json:"links"`
|
||||
Logos PluginLogos `json:"logos"`
|
||||
Version string `json:"version"`
|
||||
Updated string `json:"updated"`
|
||||
}
|
||||
|
||||
type PluginInfoLink struct {
|
||||
Name string `json:"name"`
|
||||
Url string `json:"url"`
|
||||
Path string `json:"path"`
|
||||
}
|
||||
|
||||
type PluginLogos struct {
|
||||
Small string `json:"small"`
|
||||
Large string `json:"large"`
|
||||
}
|
||||
|
||||
type PluginStaticRoute struct {
|
||||
Directory string
|
||||
PluginId string
|
||||
}
|
||||
|
||||
type ApiPluginRoute struct {
|
||||
Path string `json:"path"`
|
||||
Method string `json:"method"`
|
||||
ReqSignedIn bool `json:"reqSignedIn"`
|
||||
ReqGrafanaAdmin bool `json:"reqGrafanaAdmin"`
|
||||
ReqRole models.RoleType `json:"reqRole"`
|
||||
Url string `json:"url"`
|
||||
}
|
||||
|
||||
type ApiPlugin struct {
|
||||
PluginBase
|
||||
Routes []*ApiPluginRoute `json:"routes"`
|
||||
}
|
||||
|
||||
type EnabledPlugins struct {
|
||||
Panels []*PanelPlugin
|
||||
DataSources map[string]*DataSourcePlugin
|
||||
ApiList []*ApiPlugin
|
||||
Apps []*AppPlugin
|
||||
}
|
||||
|
||||
func NewEnabledPlugins() EnabledPlugins {
|
||||
return EnabledPlugins{
|
||||
Panels: make([]*PanelPlugin, 0),
|
||||
DataSources: make(map[string]*DataSourcePlugin),
|
||||
ApiList: make([]*ApiPlugin, 0),
|
||||
Apps: make([]*AppPlugin, 0),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
package plugins
|
||||
|
||||
import "encoding/json"
|
||||
|
||||
type PanelPlugin struct {
|
||||
FrontendPluginBase
|
||||
}
|
||||
|
||||
func (p *PanelPlugin) Load(decoder *json.Decoder, pluginDir string) error {
|
||||
if err := decoder.Decode(&p); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
p.PluginDir = pluginDir
|
||||
p.initFrontendPlugin()
|
||||
Panels[p.Id] = p
|
||||
|
||||
return nil
|
||||
}
|
||||
+78
-46
@@ -1,12 +1,16 @@
|
||||
package plugins
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"strings"
|
||||
"text/template"
|
||||
|
||||
"github.com/grafana/grafana/pkg/log"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
@@ -14,9 +18,12 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
DataSources map[string]DataSourcePlugin
|
||||
Panels map[string]PanelPlugin
|
||||
StaticRoutes []*StaticRootConfig
|
||||
DataSources map[string]*DataSourcePlugin
|
||||
Panels map[string]*PanelPlugin
|
||||
ApiPlugins map[string]*ApiPlugin
|
||||
StaticRoutes []*PluginStaticRoute
|
||||
Apps map[string]*AppPlugin
|
||||
PluginTypes map[string]interface{}
|
||||
)
|
||||
|
||||
type PluginScanner struct {
|
||||
@@ -25,18 +32,45 @@ type PluginScanner struct {
|
||||
}
|
||||
|
||||
func Init() error {
|
||||
DataSources = make(map[string]DataSourcePlugin)
|
||||
StaticRoutes = make([]*StaticRootConfig, 0)
|
||||
Panels = make(map[string]PanelPlugin)
|
||||
DataSources = make(map[string]*DataSourcePlugin)
|
||||
ApiPlugins = make(map[string]*ApiPlugin)
|
||||
StaticRoutes = make([]*PluginStaticRoute, 0)
|
||||
Panels = make(map[string]*PanelPlugin)
|
||||
Apps = make(map[string]*AppPlugin)
|
||||
PluginTypes = map[string]interface{}{
|
||||
"panel": PanelPlugin{},
|
||||
"datasource": DataSourcePlugin{},
|
||||
"api": ApiPlugin{},
|
||||
"app": AppPlugin{},
|
||||
}
|
||||
|
||||
scan(path.Join(setting.StaticRootPath, "app/plugins"))
|
||||
scan(path.Join(setting.PluginsPath))
|
||||
checkExternalPluginPaths()
|
||||
|
||||
checkPluginPaths()
|
||||
// checkDependencies()
|
||||
return nil
|
||||
}
|
||||
|
||||
func checkExternalPluginPaths() error {
|
||||
// func checkDependencies() {
|
||||
// for appType, app := range Apps {
|
||||
// for _, reqPanel := range app.PanelPlugins {
|
||||
// if _, ok := Panels[reqPanel]; !ok {
|
||||
// log.Fatal(4, "App %s requires Panel type %s, but it is not present.", appType, reqPanel)
|
||||
// }
|
||||
// }
|
||||
// for _, reqDataSource := range app.DatasourcePlugins {
|
||||
// if _, ok := DataSources[reqDataSource]; !ok {
|
||||
// log.Fatal(4, "App %s requires DataSource type %s, but it is not present.", appType, reqDataSource)
|
||||
// }
|
||||
// }
|
||||
// for _, reqApiPlugin := range app.ApiPlugins {
|
||||
// if _, ok := ApiPlugins[reqApiPlugin]; !ok {
|
||||
// log.Fatal(4, "App %s requires ApiPlugin type %s, but it is not present.", appType, reqApiPlugin)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
func checkPluginPaths() error {
|
||||
for _, section := range setting.Cfg.Sections() {
|
||||
if strings.HasPrefix(section.Name(), "plugin.") {
|
||||
path := section.Key("path").String()
|
||||
@@ -87,11 +121,26 @@ func (scanner *PluginScanner) walker(currentPath string, f os.FileInfo, err erro
|
||||
return nil
|
||||
}
|
||||
|
||||
func addStaticRoot(staticRootConfig *StaticRootConfig, currentDir string) {
|
||||
if staticRootConfig != nil {
|
||||
staticRootConfig.Path = path.Join(currentDir, staticRootConfig.Path)
|
||||
StaticRoutes = append(StaticRoutes, staticRootConfig)
|
||||
func interpolatePluginJson(reader io.Reader, pluginCommon *PluginBase) (io.Reader, error) {
|
||||
buf := new(bytes.Buffer)
|
||||
buf.ReadFrom(reader)
|
||||
jsonStr := buf.String() //
|
||||
|
||||
tmpl, err := template.New("json").Parse(jsonStr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
data := map[string]interface{}{
|
||||
"PluginPublicRoot": "public/plugins/" + pluginCommon.Id,
|
||||
}
|
||||
|
||||
var resultBuffer bytes.Buffer
|
||||
if err := tmpl.ExecuteTemplate(&resultBuffer, "json", data); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return bytes.NewReader(resultBuffer.Bytes()), nil
|
||||
}
|
||||
|
||||
func (scanner *PluginScanner) loadPluginJson(pluginJsonFilePath string) error {
|
||||
@@ -104,46 +153,29 @@ func (scanner *PluginScanner) loadPluginJson(pluginJsonFilePath string) error {
|
||||
defer reader.Close()
|
||||
|
||||
jsonParser := json.NewDecoder(reader)
|
||||
|
||||
pluginJson := make(map[string]interface{})
|
||||
if err := jsonParser.Decode(&pluginJson); err != nil {
|
||||
pluginCommon := PluginBase{}
|
||||
if err := jsonParser.Decode(&pluginCommon); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
pluginType, exists := pluginJson["pluginType"]
|
||||
if !exists {
|
||||
return errors.New("Did not find pluginType property in plugin.json")
|
||||
if pluginCommon.Id == "" || pluginCommon.Type == "" {
|
||||
return errors.New("Did not find type and id property in plugin.json")
|
||||
}
|
||||
|
||||
if pluginType == "datasource" {
|
||||
p := DataSourcePlugin{}
|
||||
reader.Seek(0, 0)
|
||||
if err := jsonParser.Decode(&p); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if p.Type == "" {
|
||||
return errors.New("Did not find type property in plugin.json")
|
||||
}
|
||||
|
||||
DataSources[p.Type] = p
|
||||
addStaticRoot(p.StaticRootConfig, currentDir)
|
||||
reader.Seek(0, 0)
|
||||
if newReader, err := interpolatePluginJson(reader, &pluginCommon); err != nil {
|
||||
return err
|
||||
} else {
|
||||
jsonParser = json.NewDecoder(newReader)
|
||||
}
|
||||
|
||||
if pluginType == "panel" {
|
||||
p := PanelPlugin{}
|
||||
reader.Seek(0, 0)
|
||||
if err := jsonParser.Decode(&p); err != nil {
|
||||
return err
|
||||
}
|
||||
var loader PluginLoader
|
||||
|
||||
if p.Type == "" {
|
||||
return errors.New("Did not find type property in plugin.json")
|
||||
}
|
||||
|
||||
Panels[p.Type] = p
|
||||
addStaticRoot(p.StaticRootConfig, currentDir)
|
||||
if pluginGoType, exists := PluginTypes[pluginCommon.Type]; !exists {
|
||||
return errors.New("Unkown plugin type " + pluginCommon.Type)
|
||||
} else {
|
||||
loader = reflect.New(reflect.TypeOf(pluginGoType)).Interface().(PluginLoader)
|
||||
}
|
||||
|
||||
return nil
|
||||
return loader.Load(jsonParser, currentDir)
|
||||
}
|
||||
|
||||
@@ -18,5 +18,22 @@ func TestPluginScans(t *testing.T) {
|
||||
|
||||
So(err, ShouldBeNil)
|
||||
So(len(DataSources), ShouldBeGreaterThan, 1)
|
||||
So(len(Panels), ShouldBeGreaterThan, 1)
|
||||
|
||||
Convey("Should set module automatically", func() {
|
||||
So(DataSources["graphite"].Module, ShouldEqual, "app/plugins/datasource/graphite/module")
|
||||
})
|
||||
})
|
||||
|
||||
Convey("When reading app plugin definition", t, func() {
|
||||
setting.Cfg = ini.Empty()
|
||||
sec, _ := setting.Cfg.NewSection("plugin.app-test")
|
||||
sec.NewKey("path", "../../tests/app-plugin-json")
|
||||
err := Init()
|
||||
|
||||
So(err, ShouldBeNil)
|
||||
So(len(Apps), ShouldBeGreaterThan, 0)
|
||||
So(Apps["app-example"].Info.Logos.Large, ShouldEqual, "public/plugins/app-example/img/logo_large.png")
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
package plugins
|
||||
|
||||
import (
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
m "github.com/grafana/grafana/pkg/models"
|
||||
)
|
||||
|
||||
func GetOrgAppSettings(orgId int64) (map[string]*m.AppSettings, error) {
|
||||
query := m.GetAppSettingsQuery{OrgId: orgId}
|
||||
|
||||
if err := bus.Dispatch(&query); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
orgAppsMap := make(map[string]*m.AppSettings)
|
||||
for _, orgApp := range query.Result {
|
||||
orgAppsMap[orgApp.AppId] = orgApp
|
||||
}
|
||||
|
||||
return orgAppsMap, nil
|
||||
}
|
||||
|
||||
func GetEnabledPlugins(orgId int64) (*EnabledPlugins, error) {
|
||||
enabledPlugins := NewEnabledPlugins()
|
||||
orgApps, err := GetOrgAppSettings(orgId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
seenPanels := make(map[string]bool)
|
||||
seenApi := make(map[string]bool)
|
||||
|
||||
for appId, installedApp := range Apps {
|
||||
var app AppPlugin
|
||||
app = *installedApp
|
||||
|
||||
// check if the app is stored in the DB for this org and if so, use the
|
||||
// state stored there.
|
||||
if b, ok := orgApps[appId]; ok {
|
||||
app.Enabled = b.Enabled
|
||||
app.Pinned = b.Pinned
|
||||
}
|
||||
|
||||
if app.Enabled {
|
||||
enabledPlugins.Apps = append(enabledPlugins.Apps, &app)
|
||||
}
|
||||
}
|
||||
|
||||
// add all plugins that are not part of an App.
|
||||
for d, installedDs := range DataSources {
|
||||
if installedDs.App == "" {
|
||||
enabledPlugins.DataSources[d] = installedDs
|
||||
}
|
||||
}
|
||||
|
||||
for p, panel := range Panels {
|
||||
if panel.App == "" {
|
||||
if _, ok := seenPanels[p]; !ok {
|
||||
seenPanels[p] = true
|
||||
enabledPlugins.Panels = append(enabledPlugins.Panels, panel)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for a, api := range ApiPlugins {
|
||||
if api.App == "" {
|
||||
if _, ok := seenApi[a]; !ok {
|
||||
seenApi[a] = true
|
||||
enabledPlugins.ApiList = append(enabledPlugins.ApiList, api)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return &enabledPlugins, nil
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package sqlstore
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
m "github.com/grafana/grafana/pkg/models"
|
||||
)
|
||||
|
||||
func init() {
|
||||
bus.AddHandler("sql", GetAppSettings)
|
||||
bus.AddHandler("sql", UpdateAppSettings)
|
||||
}
|
||||
|
||||
func GetAppSettings(query *m.GetAppSettingsQuery) error {
|
||||
sess := x.Where("org_id=?", query.OrgId)
|
||||
|
||||
query.Result = make([]*m.AppSettings, 0)
|
||||
return sess.Find(&query.Result)
|
||||
}
|
||||
|
||||
func UpdateAppSettings(cmd *m.UpdateAppSettingsCmd) error {
|
||||
return inTransaction2(func(sess *session) error {
|
||||
var app m.AppSettings
|
||||
|
||||
exists, err := sess.Where("org_id=? and app_id=?", cmd.OrgId, cmd.AppId).Get(&app)
|
||||
sess.UseBool("enabled")
|
||||
sess.UseBool("pinned")
|
||||
if !exists {
|
||||
app = m.AppSettings{
|
||||
AppId: cmd.AppId,
|
||||
OrgId: cmd.OrgId,
|
||||
Enabled: cmd.Enabled,
|
||||
Pinned: cmd.Pinned,
|
||||
JsonData: cmd.JsonData,
|
||||
Created: time.Now(),
|
||||
Updated: time.Now(),
|
||||
}
|
||||
_, err = sess.Insert(&app)
|
||||
return err
|
||||
} else {
|
||||
app.Updated = time.Now()
|
||||
app.Enabled = cmd.Enabled
|
||||
app.JsonData = cmd.JsonData
|
||||
app.Pinned = cmd.Pinned
|
||||
_, err = sess.Id(app.Id).Update(&app)
|
||||
return err
|
||||
}
|
||||
})
|
||||
}
|
||||
+9
-7
@@ -2,25 +2,27 @@ package migrations
|
||||
|
||||
import . "github.com/grafana/grafana/pkg/services/sqlstore/migrator"
|
||||
|
||||
func addPluginBundleMigration(mg *Migrator) {
|
||||
func addAppSettingsMigration(mg *Migrator) {
|
||||
|
||||
var pluginBundleV1 = Table{
|
||||
Name: "plugin_bundle",
|
||||
appSettingsV1 := Table{
|
||||
Name: "app_settings",
|
||||
Columns: []*Column{
|
||||
{Name: "id", Type: DB_BigInt, IsPrimaryKey: true, IsAutoIncrement: true},
|
||||
{Name: "org_id", Type: DB_BigInt, Nullable: true},
|
||||
{Name: "type", Type: DB_NVarchar, Length: 255, Nullable: false},
|
||||
{Name: "app_id", Type: DB_NVarchar, Length: 255, Nullable: false},
|
||||
{Name: "enabled", Type: DB_Bool, Nullable: false},
|
||||
{Name: "pinned", Type: DB_Bool, Nullable: false},
|
||||
{Name: "json_data", Type: DB_Text, Nullable: true},
|
||||
{Name: "created", Type: DB_DateTime, Nullable: false},
|
||||
{Name: "updated", Type: DB_DateTime, Nullable: false},
|
||||
},
|
||||
Indices: []*Index{
|
||||
{Cols: []string{"org_id", "type"}, Type: UniqueIndex},
|
||||
{Cols: []string{"org_id", "app_id"}, Type: UniqueIndex},
|
||||
},
|
||||
}
|
||||
mg.AddMigration("create plugin_bundle table v1", NewAddTableMigration(pluginBundleV1))
|
||||
|
||||
mg.AddMigration("create app_settings table v1", NewAddTableMigration(appSettingsV1))
|
||||
|
||||
//------- indexes ------------------
|
||||
addTableIndicesMigrations(mg, "v1", pluginBundleV1)
|
||||
addTableIndicesMigrations(mg, "v3", appSettingsV1)
|
||||
}
|
||||
@@ -18,7 +18,7 @@ func AddMigrations(mg *Migrator) {
|
||||
addApiKeyMigrations(mg)
|
||||
addDashboardSnapshotMigrations(mg)
|
||||
addQuotaMigration(mg)
|
||||
addPluginBundleMigration(mg)
|
||||
addAppSettingsMigration(mg)
|
||||
addSessionMigration(mg)
|
||||
addPlaylistMigrations(mg)
|
||||
}
|
||||
|
||||
@@ -1,46 +0,0 @@
|
||||
package sqlstore
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
m "github.com/grafana/grafana/pkg/models"
|
||||
)
|
||||
|
||||
func init() {
|
||||
bus.AddHandler("sql", GetPluginBundles)
|
||||
bus.AddHandler("sql", UpdatePluginBundle)
|
||||
}
|
||||
|
||||
func GetPluginBundles(query *m.GetPluginBundlesQuery) error {
|
||||
sess := x.Where("org_id=?", query.OrgId)
|
||||
|
||||
query.Result = make([]*m.PluginBundle, 0)
|
||||
return sess.Find(&query.Result)
|
||||
}
|
||||
|
||||
func UpdatePluginBundle(cmd *m.UpdatePluginBundleCmd) error {
|
||||
return inTransaction2(func(sess *session) error {
|
||||
var bundle m.PluginBundle
|
||||
|
||||
exists, err := sess.Where("org_id=? and type=?", cmd.OrgId, cmd.Type).Get(&bundle)
|
||||
sess.UseBool("enabled")
|
||||
if !exists {
|
||||
bundle = m.PluginBundle{
|
||||
Type: cmd.Type,
|
||||
OrgId: cmd.OrgId,
|
||||
Enabled: cmd.Enabled,
|
||||
JsonData: cmd.JsonData,
|
||||
Created: time.Now(),
|
||||
Updated: time.Now(),
|
||||
}
|
||||
_, err = sess.Insert(&bundle)
|
||||
return err
|
||||
} else {
|
||||
bundle.Enabled = cmd.Enabled
|
||||
bundle.JsonData = cmd.JsonData
|
||||
_, err = sess.Id(bundle.Id).Update(&bundle)
|
||||
return err
|
||||
}
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user