diff --git a/pkg/api/api.go b/pkg/api/api.go index 3d1b28d7aa3..f616efb59ed 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -187,5 +187,7 @@ func Register(r *macaron.Macaron) { // rendering r.Get("/render/*", reqSignedIn, RenderToPng) + InitExternalPluginRoutes(r) + r.NotFound(NotFoundHandler) } diff --git a/pkg/api/externalplugin.go b/pkg/api/externalplugin.go new file mode 100644 index 00000000000..e1ee5806b9e --- /dev/null +++ b/pkg/api/externalplugin.go @@ -0,0 +1,74 @@ +package api + +import ( + "encoding/json" + "github.com/Unknwon/macaron" + "github.com/grafana/grafana/pkg/log" + "github.com/grafana/grafana/pkg/middleware" + "github.com/grafana/grafana/pkg/plugins" + "github.com/grafana/grafana/pkg/util" + "net/http" + "net/http/httputil" + "net/url" +) + +func InitExternalPluginRoutes(r *macaron.Macaron) { + /* + // Handle Auth and role requirements + if route.ReqSignedIn { + c.Invoke(middleware.Auth(&middleware.AuthOptions{ReqSignedIn: true})) + } + if route.ReqGrafanaAdmin { + c.Invoke(middleware.Auth(&middleware.AuthOptions{ReqSignedIn: true, ReqGrafanaAdmin: true})) + } + if route.ReqRole != nil { + if *route.ReqRole == m.ROLE_EDITOR { + c.Invoke(middleware.RoleAuth(m.ROLE_EDITOR, m.ROLE_ADMIN)) + } + if *route.ReqRole == m.ROLE_ADMIN { + c.Invoke(middleware.RoleAuth(m.ROLE_ADMIN)) + } + } + */ + for _, plugin := range plugins.ExternalPlugins { + log.Info("adding routes for external plugin") + for _, route := range plugin.Settings.Routes { + log.Info("adding route %s /plugins%s", route.Method, route.Path) + r.Route(util.JoinUrlFragments("/plugins/", route.Path), route.Method, ExternalPlugin(route.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} +} diff --git a/pkg/api/index.go b/pkg/api/index.go index 556db006b2f..e41db285efc 100644 --- a/pkg/api/index.go +++ b/pkg/api/index.go @@ -3,6 +3,7 @@ package api import ( "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/middleware" + "github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/setting" ) @@ -52,6 +53,25 @@ func setIndexViewData(c *middleware.Context) error { c.Data["GoogleTagManagerId"] = setting.GoogleTagManagerId } + externalPluginJs := make([]string, 0) + externalPluginCss := make([]string, 0) + externalPluginMenu := make([]*plugins.ExternalPluginMenuItem, 0) + for _, plugin := range plugins.ExternalPlugins { + for _, js := range plugin.Settings.Js { + externalPluginJs = append(externalPluginJs, js.Src) + } + for _, css := range plugin.Settings.Css { + externalPluginCss = append(externalPluginCss, css.Href) + } + for _, item := range plugin.Settings.MenuItems { + externalPluginMenu = append(externalPluginMenu, item) + } + + } + c.Data["ExternalPluginJs"] = externalPluginJs + c.Data["ExternalPluginCss"] = externalPluginCss + c.Data["ExternalPluginMenu"] = externalPluginMenu + return nil } diff --git a/pkg/plugins/plugins.go b/pkg/plugins/plugins.go index 665cf6a36ca..323c5e87aaa 100644 --- a/pkg/plugins/plugins.go +++ b/pkg/plugins/plugins.go @@ -8,6 +8,7 @@ import ( "path/filepath" "github.com/grafana/grafana/pkg/log" + "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/setting" ) @@ -16,8 +17,44 @@ type PluginMeta struct { Name string `json:"name"` } +type ExternalPluginRoute struct { + Path string `json:"path"` + Method string `json:"method"` + ReqSignedIn bool `json:"req_signed_in"` + ReqGrafanaAdmin bool `json:"req_grafana_admin"` + ReqRole models.RoleType `json:"req_role"` + Url string `json:"url"` +} + +type ExternalPluginJs struct { + Src string `json:"src"` +} + +type ExternalPluginMenuItem struct { + Text string `json:"text"` + Icon string `json:"icon"` + Href string `json:"href"` +} + +type ExternalPluginCss struct { + Href string `json:"href"` +} + +type ExternalPluginSettings struct { + Routes []*ExternalPluginRoute `json:"routes"` + Js []*ExternalPluginJs `json:"js"` + Css []*ExternalPluginCss `json:"css"` + MenuItems []*ExternalPluginMenuItem `json:"menu_items"` +} + +type ExternalPlugin struct { + PluginType string `json:"pluginType"` + Settings ExternalPluginSettings `json:"settings"` +} + var ( - DataSources map[string]interface{} + DataSources map[string]interface{} + ExternalPlugins []ExternalPlugin ) type PluginScanner struct { @@ -31,6 +68,7 @@ func Init() { func scan(pluginDir string) error { DataSources = make(map[string]interface{}) + ExternalPlugins = make([]ExternalPlugin, 0) scanner := &PluginScanner{ pluginPath: pluginDir, @@ -93,6 +131,14 @@ func (scanner *PluginScanner) loadPluginJson(path string) error { } DataSources[datasourceType.(string)] = pluginJson } + if pluginType == "externalPlugin" { + p := ExternalPlugin{} + reader.Seek(0, 0) + if err := jsonParser.Decode(&p); err != nil { + return err + } + ExternalPlugins = append(ExternalPlugins, p) + } return nil } diff --git a/public/app/app.js b/public/app/app.js index 0637b3aeea7..784afd72f5f 100644 --- a/public/app/app.js +++ b/public/app/app.js @@ -35,6 +35,8 @@ function (angular, $, _, appLevelRequire) { } else { _.extend(module, register_fns); } + // push it into the apps dependencies + apps_deps.push(module.name); return module; }; @@ -64,8 +66,6 @@ function (angular, $, _, appLevelRequire) { var module_name = 'grafana.'+type; // create the module app.useModule(angular.module(module_name, [])); - // push it into the apps dependencies - apps_deps.push(module_name); }); var preBootRequires = [ diff --git a/public/app/core/controllers/sidemenu_ctrl.js b/public/app/core/controllers/sidemenu_ctrl.js index c2ee868323f..0e3bcb43861 100644 --- a/public/app/core/controllers/sidemenu_ctrl.js +++ b/public/app/core/controllers/sidemenu_ctrl.js @@ -28,6 +28,18 @@ function (angular, _, $, coreModule, config) { href: $scope.getUrl("/datasources"), }); } + + if (_.isArray(window.externalPlugins.MainLinks)) { + _.forEach(window.externalPlugins.MainLinks, function(item) { + if (!item.adminOnly || contextSrv.hasRole('Admin')) { + $scope.mainLinks.push({ + text: item.text, + icon: item.icon, + href: $scope.getUrl(item.href) + }); + } + }); + } }; $scope.loadOrgs = function() { diff --git a/public/app/core/routes/all.js b/public/app/core/routes/all.js index a7e36a0e228..b0e41bc956d 100644 --- a/public/app/core/routes/all.js +++ b/public/app/core/routes/all.js @@ -10,7 +10,7 @@ define([ $locationProvider.html5Mode(true); var loadOrgBundle = new BundleLoader.BundleLoader('app/features/org/all'); - + console.log("adding grafana routes"); $routeProvider .when('/', { templateUrl: 'app/partials/dashboard.html', diff --git a/public/app/plugins/externalPlugins/example/README.TXT b/public/app/plugins/externalPlugins/example/README.TXT new file mode 100644 index 00000000000..0963375e9fe --- /dev/null +++ b/public/app/plugins/externalPlugins/example/README.TXT @@ -0,0 +1,3 @@ +Example app is available at https://github.com/raintank/grafana-plugin-example + +To use, download the example app from github and run it (requires python Flask). Then rename the "_plugin.json" file in this director to "plugin.json" and restart Grafana. diff --git a/public/app/plugins/externalPlugins/example/_plugin.json b/public/app/plugins/externalPlugins/example/_plugin.json new file mode 100644 index 00000000000..a8469eadd62 --- /dev/null +++ b/public/app/plugins/externalPlugins/example/_plugin.json @@ -0,0 +1,41 @@ +{ + "pluginType": "externalPlugin", + "settings": { + "routes": [ + { + "path": "/example/static/*", + "method": "*", + "req_signed_in": false, + "req_grafana_admin": false, + "req_role": "Admin", + "url": "http://localhost:5000/static" + }, + { + "path": "/example/api/*", + "method": "*", + "req_signed_in": true, + "req_grafana_admin": false, + "req_role": "Admin", + "url": "http://localhost:5000/api" + } + ], + "css": [ + { + "href": "/example/static/css/example.css" + } + ], + "js": [ + { + "src": "/example/static/js/app.js" + } + ], + "menu_items": [ + { + "text": "Example Plugin", + "icon": "fa fa-fw fa-smile-o", + "href": "/example/servers", + "adminOnly": false, + } + ] + } +} diff --git a/public/views/index.html b/public/views/index.html index 5b4e2f5bd92..64d0fed322c 100644 --- a/public/views/index.html +++ b/public/views/index.html @@ -13,6 +13,9 @@ [[else]] [[end]] + [[ range $css := .ExternalPluginCss ]] + + [[ end ]] @@ -52,11 +55,17 @@ settings: [[.Settings]], }; + window.externalPlugins = { + MainLinks: [[.ExternalPluginMenu]] + }; + require(['app/app'], function (app) { - app.boot(); + app.boot(); }) - + [[ range $js := .ExternalPluginJs]] + + [[ end ]] [[if .GoogleAnalyticsId]]