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]]