diff --git a/pkg/api/api.go b/pkg/api/api.go index 27eb3c749db..e9d85dc0c8d 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -185,5 +185,7 @@ func Register(r *macaron.Macaron) { // rendering r.Get("/render/*", reqSignedIn, RenderToPng) + InitThirdPartyRoutes(r) + r.NotFound(NotFoundHandler) } diff --git a/pkg/api/index.go b/pkg/api/index.go index 556db006b2f..072878a2cfd 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" ) @@ -51,6 +52,25 @@ func setIndexViewData(c *middleware.Context) error { if setting.GoogleTagManagerId != "" { c.Data["GoogleTagManagerId"] = setting.GoogleTagManagerId } + // This can be loaded from the DB/file to allow 3rdParty integration + thirdPartyJs := make([]string, 0) + thirdPartyCss := make([]string, 0) + thirdPartyMenu := make([]*plugins.ThirdPartyMenuItem, 0) + for _, integration := range plugins.Integrations { + for _, js := range integration.Js { + thirdPartyJs = append(thirdPartyJs, js.Src) + } + for _, css := range integration.Css { + thirdPartyCss = append(thirdPartyCss, css.Href) + } + for _, item := range integration.MenuItems { + thirdPartyMenu = append(thirdPartyMenu, item) + } + + } + c.Data["ThirdPartyJs"] = thirdPartyJs + c.Data["ThirdPartyCss"] = thirdPartyCss + c.Data["ThirdPartyMenu"] = thirdPartyMenu return nil } diff --git a/pkg/api/thirdparty.go b/pkg/api/thirdparty.go new file mode 100644 index 00000000000..588c3b40ce7 --- /dev/null +++ b/pkg/api/thirdparty.go @@ -0,0 +1,75 @@ +package api + +import ( + "encoding/json" + "github.com/Unknwon/macaron" + "github.com/grafana/grafana/pkg/middleware" + "github.com/grafana/grafana/pkg/plugins" + "github.com/grafana/grafana/pkg/util" + "log" + "net/http" + "net/http/httputil" + "net/url" +) + +func InitThirdPartyRoutes(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 _, integration := range plugins.Integrations { + log.Printf("adding routes for integration") + for _, route := range integration.Routes { + log.Printf("adding route %s %s", route.Method, route.Path) + r.Route(util.JoinUrlFragments("/thirdparty/", route.Path), route.Method, ThirdParty(route.Url)) + } + } +} + +func ThirdParty(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, "Not found", err) + return + } + log.Printf(string(ctx)) + targetUrl, _ := url.Parse(routeUrl) + proxy := NewThirdPartyProxy(string(ctx), path, targetUrl) + proxy.Transport = dataProxyTransport + proxy.ServeHTTP(c.RW(), c.Req.Request) + } +} + +func NewThirdPartyProxy(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/models/third_party.go b/pkg/models/third_party.go new file mode 100644 index 00000000000..0bce0d86f8f --- /dev/null +++ b/pkg/models/third_party.go @@ -0,0 +1,31 @@ +package models + +type ThirdPartyRoute struct { + Path string `json:"path"` + Method string `json:"method"` + ReqSignedIn bool `json:"req_signed_in"` + ReqGrafanaAdmin bool `json:"req_grafana_admin"` + ReqRole RoleType `json:"req_role"` + Url string `json:"url"` +} + +type ThirdPartyJs struct { + src string `json:"src"` +} + +type ThirdPartyMenuItem struct { + Text string `json:"text"` + Icon string `json:"icon"` + Href string `json:"href"` +} + +type ThirdPartyCss struct { + Href string `json:"href"` +} + +type ThirdPartyIntegration struct { + Routes []*ThirdPartyRoute `json:"routes"` + Js []*ThirdPartyJs `json:"js"` + Css []*ThirdPartyCss `json:"css"` + MenuItems []*ThirdPartyMenuItem `json:"menu_items"` +} diff --git a/pkg/plugins/plugins.go b/pkg/plugins/plugins.go index 2f7e5264e53..c411a47a977 100644 --- a/pkg/plugins/plugins.go +++ b/pkg/plugins/plugins.go @@ -16,8 +16,44 @@ type PluginMeta struct { Name string `json:"name"` } +type ThirdPartyRoute struct { + Path string `json:"path"` + Method string `json:"method"` + ReqSignedIn bool `json:"req_signed_in"` + ReqGrafanaAdmin bool `json:"req_grafana_admin"` + ReqRole string `json:"req_role"` + Url string `json:"url"` +} + +type ThirdPartyJs struct { + Src string `json:"src"` +} + +type ThirdPartyMenuItem struct { + Text string `json:"text"` + Icon string `json:"icon"` + Href string `json:"href"` +} + +type ThirdPartyCss struct { + Href string `json:"href"` +} + +type ThirdPartyIntegration struct { + Routes []*ThirdPartyRoute `json:"routes"` + Js []*ThirdPartyJs `json:"js"` + Css []*ThirdPartyCss `json:"css"` + MenuItems []*ThirdPartyMenuItem `json:"menu_items"` +} + +type ThirdPartyPlugin struct { + PluginType string `json:"pluginType"` + Integration ThirdPartyIntegration `json:"integration"` +} + var ( - DataSources map[string]interface{} + DataSources map[string]interface{} + Integrations []ThirdPartyIntegration ) type PluginScanner struct { @@ -31,6 +67,7 @@ func Init() { func scan(pluginDir string) error { DataSources = make(map[string]interface{}) + Integrations = make([]ThirdPartyIntegration, 0) scanner := &PluginScanner{ pluginPath: pluginDir, @@ -94,6 +131,14 @@ func (scanner *PluginScanner) loadPluginJson(path string) error { DataSources[datasourceType.(string)] = pluginJson } + if pluginType == "thirdPartyIntegration" { + p := ThirdPartyPlugin{} + reader.Seek(0, 0) + if err := jsonParser.Decode(&p); err != nil { + return err + } + Integrations = append(Integrations, p.Integration) + } return nil } diff --git a/public/app/app.js b/public/app/app.js index 4f30df34d89..16ce8a454d8 100644 --- a/public/app/app.js +++ b/public/app/app.js @@ -37,6 +37,8 @@ function (angular, $, _, appLevelRequire) { } else { _.extend(module, register_fns); } + // push it into the apps dependencies + apps_deps.push(module.name); return module; }; @@ -66,8 +68,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/controllers/sidemenuCtrl.js b/public/app/controllers/sidemenuCtrl.js index b7ba32f0d35..bd6538b15cb 100644 --- a/public/app/controllers/sidemenuCtrl.js +++ b/public/app/controllers/sidemenuCtrl.js @@ -29,6 +29,16 @@ function (angular, _, $, config) { href: $scope.getUrl("/datasources"), }); } + + if (_.isArray(window.thirdParty.MainLinks)) { + _.forEach(window.thirdParty.MainLinks, function(item) { + $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/thirdPartyIntegration/raintank/plugin.json b/public/app/plugins/thirdPartyIntegration/raintank/plugin.json new file mode 100644 index 00000000000..7c1dcb0dcee --- /dev/null +++ b/public/app/plugins/thirdPartyIntegration/raintank/plugin.json @@ -0,0 +1,40 @@ +{ + "pluginType": "thirdPartyIntegration", + "integration": { + "routes": [ + { + "path": "/raintank/public/*", + "method": "*", + "req_signed_in": false, + "req_grafana_admin": false, + "req_role": "Admin", + "url": "http://localhost:3001/public" + }, + { + "path": "/raintank/api/*", + "method": "*", + "req_signed_in": true, + "req_grafana_admin": false, + "req_role": "Admin", + "url": "http://localhost:3001/api" + } + ], + "css": [ + { + "href": "/path/to/file.css" + } + ], + "js": [ + { + "src": "/raintank/public/app.js" + } + ], + "menu_items": [ + { + "text": "Menu Text", + "icon": "fa fa-fw fa-database", + "href": "/raintank/test" + } + ] + } +} diff --git a/public/views/index.html b/public/views/index.html index 600e18fb954..9998b0d2b68 100644 --- a/public/views/index.html +++ b/public/views/index.html @@ -13,6 +13,9 @@ [[else]] [[end]] + [[ range $css := .ThirdPartyCss ]] + + [[ end ]] @@ -53,11 +56,17 @@ settings: [[.Settings]], }; + window.thirdParty = { + MainLinks: [[.ThirdPartyMenu]] + }; + require(['app/app'], function (app) { - app.boot(); + app.boot(); }) - + [[ range $js := .ThirdPartyJs]] + + [[ end ]] [[if .GoogleAnalyticsId]]