diff --git a/examples/nginx-app/.gitignore b/examples/nginx-app/.gitignore
new file mode 100644
index 00000000000..8c2c350441b
--- /dev/null
+++ b/examples/nginx-app/.gitignore
@@ -0,0 +1,7 @@
+.DS_Store
+
+node_modules
+tmp/*
+npm-debug.log
+dist/*
+
diff --git a/examples/nginx-app/.jscs.json b/examples/nginx-app/.jscs.json
new file mode 100644
index 00000000000..dcf694dcc63
--- /dev/null
+++ b/examples/nginx-app/.jscs.json
@@ -0,0 +1,13 @@
+{
+ "disallowImplicitTypeConversion": ["string"],
+ "disallowKeywords": ["with"],
+ "disallowMultipleLineBreaks": true,
+ "disallowMixedSpacesAndTabs": true,
+ "disallowTrailingWhitespace": true,
+ "requireSpacesInFunctionExpression": {
+ "beforeOpeningCurlyBrace": true
+ },
+ "disallowSpacesInsideArrayBrackets": true,
+ "disallowSpacesInsideParentheses": true,
+ "validateIndentation": 2
+}
\ No newline at end of file
diff --git a/examples/nginx-app/.jshintrc b/examples/nginx-app/.jshintrc
new file mode 100644
index 00000000000..3725af83afc
--- /dev/null
+++ b/examples/nginx-app/.jshintrc
@@ -0,0 +1,36 @@
+{
+ "browser": true,
+ "esnext": true,
+
+ "bitwise":false,
+ "curly": true,
+ "eqnull": true,
+ "devel": true,
+ "eqeqeq": true,
+ "forin": false,
+ "immed": true,
+ "supernew": true,
+ "expr": true,
+ "indent": 2,
+ "latedef": true,
+ "newcap": true,
+ "noarg": true,
+ "noempty": true,
+ "undef": true,
+ "boss": true,
+ "trailing": true,
+ "laxbreak": true,
+ "laxcomma": true,
+ "sub": true,
+ "unused": true,
+ "maxdepth": 6,
+ "maxlen": 140,
+
+ "globals": {
+ "System": true,
+ "define": true,
+ "require": true,
+ "Chromath": false,
+ "setImmediate": true
+ }
+}
diff --git a/examples/nginx-app/Gruntfile.js b/examples/nginx-app/Gruntfile.js
new file mode 100644
index 00000000000..d36a4716f31
--- /dev/null
+++ b/examples/nginx-app/Gruntfile.js
@@ -0,0 +1,54 @@
+module.exports = function(grunt) {
+
+ require('load-grunt-tasks')(grunt);
+
+ grunt.loadNpmTasks('grunt-execute');
+ grunt.loadNpmTasks('grunt-contrib-clean');
+
+ grunt.initConfig({
+
+ clean: ["dist"],
+
+ copy: {
+ src_to_dist: {
+ cwd: 'src',
+ expand: true,
+ src: ['**/*', '!**/*.js', '!**/*.scss'],
+ dest: 'dist'
+ },
+ pluginDef: {
+ expand: true,
+ src: 'plugin.json',
+ dest: 'dist',
+ }
+ },
+
+ watch: {
+ rebuild_all: {
+ files: ['src/**/*', 'plugin.json'],
+ tasks: ['default'],
+ options: {spawn: false}
+ },
+ },
+
+ babel: {
+ options: {
+ sourceMap: true,
+ presets: ["es2015"],
+ plugins: ['transform-es2015-modules-systemjs', "transform-es2015-for-of"],
+ },
+ dist: {
+ files: [{
+ cwd: 'src',
+ expand: true,
+ src: ['**/*.js'],
+ dest: 'dist',
+ ext:'.js'
+ }]
+ },
+ },
+
+ });
+
+ grunt.registerTask('default', ['clean', 'copy:src_to_dist', 'copy:pluginDef', 'babel']);
+};
diff --git a/examples/nginx-app/module.js b/examples/nginx-app/module.js
deleted file mode 100644
index 9c1cccb2e52..00000000000
--- a/examples/nginx-app/module.js
+++ /dev/null
@@ -1,20 +0,0 @@
-define([
-], function() {
- 'use strict';
-
- function StreamPageCtrl() {}
- StreamPageCtrl.templateUrl = 'partials/stream.html';
-
- function LogsPageCtrl() {}
- LogsPageCtrl.templateUrl = 'partials/logs.html';
-
- function NginxConfigCtrl() {}
- NginxConfigCtrl.templateUrl = 'partials/config.html';
-
- return {
- ConfigCtrl: NginxConfigCtrl,
- StreamPageCtrl: StreamPageCtrl,
- LogsPageCtrl: LogsPageCtrl,
- };
-
-});
diff --git a/examples/nginx-app/package.json b/examples/nginx-app/package.json
new file mode 100644
index 00000000000..91c53734ec8
--- /dev/null
+++ b/examples/nginx-app/package.json
@@ -0,0 +1,37 @@
+{
+ "name": "kentik-app",
+ "private": true,
+ "version": "1.0.0",
+ "description": "",
+ "main": "index.js",
+ "scripts": {
+ "test": "echo \"Error: no test specified\" && exit 1"
+ },
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/raintank/kentik-app-poc.git"
+ },
+ "author": "",
+ "license": "ISC",
+ "bugs": {
+ "url": "https://github.com/raintank/kentik-app-poc/issues"
+ },
+ "devDependencies": {
+ "grunt": "~0.4.5",
+ "babel": "~6.5.1",
+ "grunt-babel": "~6.0.0",
+ "grunt-contrib-copy": "~0.8.2",
+ "grunt-contrib-watch": "^0.6.1",
+ "grunt-contrib-uglify": "~0.11.0",
+ "grunt-systemjs-builder": "^0.2.5",
+ "load-grunt-tasks": "~3.2.0",
+ "grunt-execute": "~0.2.2",
+ "grunt-contrib-clean": "~0.6.0"
+ },
+ "dependencies": {
+ "babel-plugin-transform-es2015-modules-systemjs": "^6.5.0",
+ "babel-preset-es2015": "^6.5.0",
+ "lodash": "~4.0.0"
+ },
+ "homepage": "https://github.com/raintank/kentik-app-poc#readme"
+}
diff --git a/examples/nginx-app/panel/module.js b/examples/nginx-app/panel/module.js
deleted file mode 100644
index 9ccc5b6c962..00000000000
--- a/examples/nginx-app/panel/module.js
+++ /dev/null
@@ -1,21 +0,0 @@
-define([
- 'app/plugins/sdk'
-], function(sdk) {
- 'use strict';
-
- var NginxPanel = (function(_super) {
- function NginxPanel($scope, $injector) {
- _super.call(this, $scope, $injector);
- }
-
- NginxPanel.template = '
nginx! ';
- NginxPanel.prototype = Object.create(_super.prototype);
- NginxPanel.prototype.constructor = NginxPanel;
-
- return NginxPanel;
- })(sdk.PanelCtrl);
-
- return {
- PanelCtrl: NginxPanel
- };
-});
diff --git a/examples/nginx-app/partials/config.html b/examples/nginx-app/partials/config.html
deleted file mode 100644
index cdd6b8d9b60..00000000000
--- a/examples/nginx-app/partials/config.html
+++ /dev/null
@@ -1 +0,0 @@
-nginx config
diff --git a/examples/nginx-app/partials/logs.html b/examples/nginx-app/partials/logs.html
deleted file mode 100644
index f6bef036395..00000000000
--- a/examples/nginx-app/partials/logs.html
+++ /dev/null
@@ -1,2 +0,0 @@
-
-Logs!
diff --git a/examples/nginx-app/partials/stream.html b/examples/nginx-app/partials/stream.html
deleted file mode 100644
index 092c36cc24c..00000000000
--- a/examples/nginx-app/partials/stream.html
+++ /dev/null
@@ -1 +0,0 @@
-streams!
diff --git a/examples/nginx-app/src/components/config.html b/examples/nginx-app/src/components/config.html
new file mode 100644
index 00000000000..c531ec36d76
--- /dev/null
+++ b/examples/nginx-app/src/components/config.html
@@ -0,0 +1,3 @@
+
+ Nginx config!
+
diff --git a/examples/nginx-app/src/components/config.js b/examples/nginx-app/src/components/config.js
new file mode 100644
index 00000000000..bb8f007b9bc
--- /dev/null
+++ b/examples/nginx-app/src/components/config.js
@@ -0,0 +1,6 @@
+
+export class NginxAppConfigCtrl {
+}
+NginxAppConfigCtrl.templateUrl = 'components/config.html';
+
+
diff --git a/examples/nginx-app/src/components/logs.html b/examples/nginx-app/src/components/logs.html
new file mode 100644
index 00000000000..ca215772bf5
--- /dev/null
+++ b/examples/nginx-app/src/components/logs.html
@@ -0,0 +1,3 @@
+
+ Logs page!
+
diff --git a/examples/nginx-app/src/components/logs.js b/examples/nginx-app/src/components/logs.js
new file mode 100644
index 00000000000..5b67290381b
--- /dev/null
+++ b/examples/nginx-app/src/components/logs.js
@@ -0,0 +1,6 @@
+
+export class LogsPageCtrl {
+}
+LogsPageCtrl.templateUrl = 'components/logs.html';
+
+
diff --git a/examples/nginx-app/src/components/stream.html b/examples/nginx-app/src/components/stream.html
new file mode 100644
index 00000000000..ad70ca4df50
--- /dev/null
+++ b/examples/nginx-app/src/components/stream.html
@@ -0,0 +1,3 @@
+
+ Stream page!
+
diff --git a/examples/nginx-app/src/components/stream.js b/examples/nginx-app/src/components/stream.js
new file mode 100644
index 00000000000..8684b36c64d
--- /dev/null
+++ b/examples/nginx-app/src/components/stream.js
@@ -0,0 +1,6 @@
+
+export class StreamPageCtrl {
+}
+StreamPageCtrl.templateUrl = 'components/stream.html';
+
+
diff --git a/examples/nginx-app/css/dark.css b/examples/nginx-app/src/css/dark.css
similarity index 100%
rename from examples/nginx-app/css/dark.css
rename to examples/nginx-app/src/css/dark.css
diff --git a/examples/nginx-app/css/light.css b/examples/nginx-app/src/css/light.css
similarity index 100%
rename from examples/nginx-app/css/light.css
rename to examples/nginx-app/src/css/light.css
diff --git a/examples/nginx-app/src/dashboards/dashboard.js b/examples/nginx-app/src/dashboards/dashboard.js
new file mode 100644
index 00000000000..794e2c5217b
--- /dev/null
+++ b/examples/nginx-app/src/dashboards/dashboard.js
@@ -0,0 +1,17 @@
+require([
+], function () {
+
+ function Dashboard() {
+
+ this.getInputs = function() {
+
+ };
+
+ this.buildDashboard = function() {
+
+ };
+ }
+
+ return Dashboard;
+});
+
diff --git a/examples/nginx-app/img/logo_large.png b/examples/nginx-app/src/img/logo_large.png
similarity index 100%
rename from examples/nginx-app/img/logo_large.png
rename to examples/nginx-app/src/img/logo_large.png
diff --git a/examples/nginx-app/img/logo_small.png b/examples/nginx-app/src/img/logo_small.png
similarity index 100%
rename from examples/nginx-app/img/logo_small.png
rename to examples/nginx-app/src/img/logo_small.png
diff --git a/examples/nginx-app/src/module.js b/examples/nginx-app/src/module.js
new file mode 100644
index 00000000000..b5aeecc6ccf
--- /dev/null
+++ b/examples/nginx-app/src/module.js
@@ -0,0 +1,9 @@
+import {LogsPageCtrl} from './components/logs';
+import {StreamPageCtrl} from './components/stream';
+import {NginxAppConfigCtrl} from './components/config';
+
+export {
+ NginxAppConfigCtrl as ConfigCtrl,
+ StreamPageCtrl,
+ LogsPageCtrl
+};
diff --git a/examples/nginx-app/src/panel/module.js b/examples/nginx-app/src/panel/module.js
new file mode 100644
index 00000000000..899586da81b
--- /dev/null
+++ b/examples/nginx-app/src/panel/module.js
@@ -0,0 +1,15 @@
+import {PanelCtrl} from 'app/plugins/sdk';
+
+class NginxPanelCtrl extends PanelCtrl {
+
+ constructor($scope, $injector) {
+ super($scope, $injector);
+ }
+
+}
+NginxPanelCtrl.template = 'nginx! ';
+
+export {
+ NginxPanelCtrl as PanelCtrl
+};
+
diff --git a/examples/nginx-app/panel/plugin.json b/examples/nginx-app/src/panel/plugin.json
similarity index 100%
rename from examples/nginx-app/panel/plugin.json
rename to examples/nginx-app/src/panel/plugin.json
diff --git a/package.json b/package.json
index 7a358ce3e04..2331bf0fe20 100644
--- a/package.json
+++ b/package.json
@@ -50,7 +50,7 @@
"phantomjs": "^1.9.19",
"reflect-metadata": "0.1.2",
"rxjs": "5.0.0-beta.0",
- "systemjs": "0.19.6",
+ "systemjs": "0.19.20",
"zone.js": "0.5.10"
},
"engines": {
@@ -59,8 +59,7 @@
},
"scripts": {
"test": "grunt test",
- "coveralls": "grunt karma:coveralls && rm -rf ./coverage",
- "postinstall": "./node_modules/.bin/grunt copy:node_modules"
+ "coveralls": "grunt karma:coveralls && rm -rf ./coverage"
},
"license": "Apache-2.0",
"dependencies": {
@@ -68,9 +67,9 @@
"grunt-jscs": "~1.5.x",
"grunt-sync": "^0.4.1",
"karma-sinon": "^1.0.3",
- "lodash": "^2.4.1",
+ "lodash": "^4.0.0",
"sinon": "1.16.1",
- "systemjs-builder": "^0.14.15",
+ "systemjs-builder": "^0.15.7",
"tslint": "^3.2.1",
"typescript": "^1.7.5"
}
diff --git a/pkg/api/app_routes.go b/pkg/api/app_routes.go
index 169c5c6d15c..5796f09bb21 100644
--- a/pkg/api/app_routes.go
+++ b/pkg/api/app_routes.go
@@ -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}
-}
diff --git a/pkg/api/cloudwatch/metrics.go b/pkg/api/cloudwatch/metrics.go
index 7a98386347c..b4321791926 100644
--- a/pkg/api/cloudwatch/metrics.go
+++ b/pkg/api/cloudwatch/metrics.go
@@ -126,6 +126,11 @@ func handleGetNamespaces(req *cwRequest, c *middleware.Context) {
for key := range metricsMap {
keys = append(keys, key)
}
+ if customMetricsNamespaces, ok := req.DataSource.JsonData["customMetricsNamespaces"].(string); ok {
+ for _, key := range strings.Split(customMetricsNamespaces, ",") {
+ keys = append(keys, key)
+ }
+ }
sort.Sort(sort.StringSlice(keys))
result := []interface{}{}
diff --git a/pkg/api/dtos/index.go b/pkg/api/dtos/index.go
index 180ec767281..7c28d734be4 100644
--- a/pkg/api/dtos/index.go
+++ b/pkg/api/dtos/index.go
@@ -7,10 +7,7 @@ type IndexViewData struct {
AppSubUrl string
GoogleAnalyticsId string
GoogleTagManagerId string
-
- PluginCss []*PluginCss
- PluginModules []string
- MainNavLinks []*NavLink
+ MainNavLinks []*NavLink
}
type PluginCss struct {
diff --git a/pkg/api/index.go b/pkg/api/index.go
index 094511df1ea..11b1107674e 100644
--- a/pkg/api/index.go
+++ b/pkg/api/index.go
@@ -81,10 +81,6 @@ func setIndexViewData(c *middleware.Context) (*dtos.IndexViewData, error) {
}
for _, plugin := range enabledPlugins.Apps {
- if plugin.Css != nil {
- data.PluginCss = append(data.PluginCss, &dtos.PluginCss{Light: plugin.Css.Light, Dark: plugin.Css.Dark})
- }
-
if plugin.Pinned {
pageLink := &dtos.NavLink{
Text: plugin.Name,
diff --git a/pkg/api/org.go b/pkg/api/org.go
index 0316116b6d3..61af62311c6 100644
--- a/pkg/api/org.go
+++ b/pkg/api/org.go
@@ -84,7 +84,7 @@ func CreateOrg(c *middleware.Context, cmd m.CreateOrgCommand) Response {
cmd.UserId = c.UserId
if err := bus.Dispatch(&cmd); err != nil {
if err == m.ErrOrgNameTaken {
- return ApiError(400, "Organization name taken", err)
+ return ApiError(409, "Organization name taken", err)
}
return ApiError(500, "Failed to create organization", err)
}
diff --git a/pkg/api/pluginproxy/pluginproxy.go b/pkg/api/pluginproxy/pluginproxy.go
new file mode 100644
index 00000000000..55aa0c013ce
--- /dev/null
+++ b/pkg/api/pluginproxy/pluginproxy.go
@@ -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}
+}
diff --git a/pkg/api/pluginproxy/pluginproxy_test.go b/pkg/api/pluginproxy/pluginproxy_test.go
new file mode 100644
index 00000000000..0b6e9523ffd
--- /dev/null
+++ b/pkg/api/pluginproxy/pluginproxy_test.go
@@ -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")
+ })
+ })
+
+}
diff --git a/pkg/models/app_settings.go b/pkg/models/app_settings.go
index 78d4c483f2b..6a7bbde694d 100644
--- a/pkg/models/app_settings.go
+++ b/pkg/models/app_settings.go
@@ -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 {
diff --git a/pkg/plugins/app_plugin.go b/pkg/plugins/app_plugin.go
index 474b7354762..190ce8a9632 100644
--- a/pkg/plugins/app_plugin.go
+++ b/pkg/plugins/app_plugin.go
@@ -28,7 +28,6 @@ type AppIncludeInfo struct {
type AppPlugin struct {
FrontendPluginBase
- Css *AppPluginCss `json:"css"`
Pages []*AppPluginPage `json:"pages"`
Routes []*AppPluginRoute `json:"routes"`
Includes []*AppIncludeInfo `json:"-"`
@@ -40,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"`
@@ -68,11 +66,6 @@ func (app *AppPlugin) Load(decoder *json.Decoder, pluginDir string) error {
func (app *AppPlugin) initApp() {
app.initFrontendPlugin()
- if app.Css != nil {
- app.Css.Dark = evalRelativePluginUrlPath(app.Css.Dark, app.Id)
- app.Css.Light = evalRelativePluginUrlPath(app.Css.Light, app.Id)
- }
-
// check if we have child panels
for _, panel := range Panels {
if strings.HasPrefix(panel.PluginDir, app.PluginDir) {
diff --git a/pkg/services/sqlstore/app_settings.go b/pkg/services/sqlstore/app_settings.go
index f454d2cc5ff..e7d8a90a495 100644
--- a/pkg/services/sqlstore/app_settings.go
+++ b/pkg/services/sqlstore/app_settings.go
@@ -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
diff --git a/pkg/services/sqlstore/sqlstore.go b/pkg/services/sqlstore/sqlstore.go
index 3ab4bf1c00b..e3db2d7f537 100644
--- a/pkg/services/sqlstore/sqlstore.go
+++ b/pkg/services/sqlstore/sqlstore.go
@@ -149,8 +149,13 @@ func getEngine() (*xorm.Engine, error) {
if len(fields) > 1 && len(strings.TrimSpace(fields[1])) > 0 {
port = fields[1]
}
- cnnstr = fmt.Sprintf("user=%s password=%s host=%s port=%s dbname=%s sslmode=%s",
- DbCfg.User, DbCfg.Pwd, host, port, DbCfg.Name, DbCfg.SslMode)
+ if DbCfg.Pwd == "" {
+ DbCfg.Pwd = "''"
+ }
+ if DbCfg.User == "" {
+ DbCfg.User = "''"
+ }
+ cnnstr = fmt.Sprintf("user=%s password=%s host=%s port=%s dbname=%s sslmode=%s", DbCfg.User, DbCfg.Pwd, host, port, DbCfg.Name, DbCfg.SslMode)
case "sqlite3":
if !filepath.IsAbs(DbCfg.Path) {
DbCfg.Path = filepath.Join(setting.DataPath, DbCfg.Path)
diff --git a/public/app/core/controllers/json_editor_ctrl.js b/public/app/core/controllers/json_editor_ctrl.js
index f83a2ac3e8c..7d7d56fa96b 100644
--- a/public/app/core/controllers/json_editor_ctrl.js
+++ b/public/app/core/controllers/json_editor_ctrl.js
@@ -8,7 +8,7 @@ function (angular, coreModule) {
coreModule.default.controller('JsonEditorCtrl', function($scope) {
$scope.json = angular.toJson($scope.object, true);
- $scope.canUpdate = $scope.updateHandler !== void 0;
+ $scope.canUpdate = $scope.updateHandler !== void 0 && $scope.contextSrv.isEditor;
$scope.update = function () {
var newObject = angular.fromJson($scope.json);
diff --git a/public/app/core/routes/routes.ts b/public/app/core/routes/routes.ts
index 8963228210b..82da84fbc56 100644
--- a/public/app/core/routes/routes.ts
+++ b/public/app/core/routes/routes.ts
@@ -45,6 +45,7 @@ function setupAngularRoutes($routeProvider, $locationProvider) {
.when('/datasources', {
templateUrl: 'public/app/features/datasources/partials/list.html',
controller : 'DataSourcesCtrl',
+ controllerAs: 'ctrl',
resolve: loadOrgBundle,
})
.when('/datasources/edit/:id', {
diff --git a/public/app/core/utils/css_loader.ts b/public/app/core/utils/css_loader.ts
new file mode 100644
index 00000000000..e81e659032d
--- /dev/null
+++ b/public/app/core/utils/css_loader.ts
@@ -0,0 +1,78 @@
+///
+
+var waitSeconds = 100;
+var head = document.getElementsByTagName('head')[0];
+
+// get all link tags in the page
+var links = document.getElementsByTagName('link');
+var linkHrefs = [];
+for (var i = 0; i < links.length; i++) {
+ linkHrefs.push(links[i].href);
+}
+
+var isWebkit = !!window.navigator.userAgent.match(/AppleWebKit\/([^ ;]*)/);
+var webkitLoadCheck = function(link, callback) {
+ setTimeout(function() {
+ for (var i = 0; i < document.styleSheets.length; i++) {
+ var sheet = document.styleSheets[i];
+ if (sheet.href === link.href) {
+ return callback();
+ }
+ }
+ webkitLoadCheck(link, callback);
+ }, 10);
+};
+
+var noop = function() {};
+
+var loadCSS = function(url) {
+ return new Promise(function(resolve, reject) {
+ var link = document.createElement('link');
+ var timeout = setTimeout(function() {
+ reject('Unable to load CSS');
+ }, waitSeconds * 1000);
+
+ var _callback = function(error) {
+ clearTimeout(timeout);
+ link.onload = link.onerror = noop;
+ setTimeout(function() {
+ if (error) {
+ reject(error);
+ } else {
+ resolve('');
+ }
+ }, 7);
+ };
+
+ link.type = 'text/css';
+ link.rel = 'stylesheet';
+ link.href = url;
+
+ if (!isWebkit) {
+ link.onload = function() { _callback(undefined); };
+ } else {
+ webkitLoadCheck(link, _callback);
+ }
+
+ link.onerror = function(evt: any) {
+ _callback(evt.error || new Error('Error loading CSS file.'));
+ };
+
+ head.appendChild(link);
+ });
+};
+
+export function fetch(load): any {
+ if (typeof window === 'undefined') {
+ return '';
+ }
+
+ // dont reload styles loaded in the head
+ for (var i = 0; i < linkHrefs.length; i++) {
+ if (load.address === linkHrefs[i]) {
+ return '';
+ }
+ }
+ return loadCSS(load.address);
+}
+
diff --git a/public/app/features/dashboard/shareSnapshotCtrl.js b/public/app/features/dashboard/shareSnapshotCtrl.js
index 44c2b3974a6..853e3db3b9c 100644
--- a/public/app/features/dashboard/shareSnapshotCtrl.js
+++ b/public/app/features/dashboard/shareSnapshotCtrl.js
@@ -117,7 +117,7 @@ function (angular, _) {
// remove template queries
_.each(dash.templating.list, function(variable) {
variable.query = "";
- variable.options = [];
+ variable.options = variable.current;
variable.refresh = false;
});
diff --git a/public/app/features/datasources/list_ctrl.js b/public/app/features/datasources/list_ctrl.js
deleted file mode 100644
index da77df252e1..00000000000
--- a/public/app/features/datasources/list_ctrl.js
+++ /dev/null
@@ -1,36 +0,0 @@
-define([
- 'angular',
- 'lodash',
-],
-function (angular) {
- 'use strict';
-
- var module = angular.module('grafana.controllers');
-
- module.controller('DataSourcesCtrl', function($scope, $http, backendSrv, datasourceSrv) {
-
- $scope.init = function() {
- $scope.datasources = [];
- $scope.getDatasources();
- };
-
- $scope.getDatasources = function() {
- backendSrv.get('/api/datasources').then(function(results) {
- $scope.datasources = results;
- });
- };
-
- $scope.remove = function(ds) {
- backendSrv.delete('/api/datasources/' + ds.id).then(function() {
- $scope.getDatasources();
-
- backendSrv.get('/api/frontend/settings').then(function(settings) {
- datasourceSrv.init(settings.datasources);
- });
- });
- };
-
- $scope.init();
-
- });
-});
diff --git a/public/app/features/datasources/list_ctrl.ts b/public/app/features/datasources/list_ctrl.ts
new file mode 100644
index 00000000000..b1f93f1a158
--- /dev/null
+++ b/public/app/features/datasources/list_ctrl.ts
@@ -0,0 +1,52 @@
+///
+
+import angular from 'angular';
+import _ from 'lodash';
+import coreModule from '../../core/core_module';
+
+export class DataSourcesCtrl {
+ datasources: any;
+
+ /** @ngInject */
+ constructor(private $scope, private $location, private $http, private backendSrv, private datasourceSrv) {
+ backendSrv.get('/api/datasources')
+ .then((result) => {
+ this.datasources = result;
+ });
+ }
+
+ removeDataSourceConfirmed(ds) {
+
+ this.backendSrv.delete('/api/datasources/' + ds.id)
+ .then(() => {
+ this.$scope.appEvent('alert-success', ['Datasource deleted', '']);
+ }, () => {
+ this.$scope.appEvent('alert-error', ['Unable to delete datasource', '']);
+ }).then(() => {
+ this.backendSrv.get('/api/datasources')
+ .then((result) => {
+ this.datasources = result;
+ });
+ this.backendSrv.get('/api/frontend/settings')
+ .then((settings) => {
+ this.datasourceSrv.init(settings.datasources);
+ });
+ });
+ }
+
+ removeDataSource(ds) {
+
+ this.$scope.appEvent('confirm-modal', {
+ title: 'Confirm delete datasource',
+ text: 'Are you sure you want to delete datasource ' + ds.name + '?',
+ yesText: "Delete",
+ icon: "fa-warning",
+ onConfirm: () => {
+ this.removeDataSourceConfirmed(ds);
+ }
+ });
+ }
+
+}
+
+coreModule.controller('DataSourcesCtrl', DataSourcesCtrl);
diff --git a/public/app/features/datasources/partials/list.html b/public/app/features/datasources/partials/list.html
index ec660ef79d4..bdd6fe363a0 100644
--- a/public/app/features/datasources/partials/list.html
+++ b/public/app/features/datasources/partials/list.html
@@ -12,11 +12,11 @@
Data sources
-
+
No data sources defined
-
+
Name
@@ -27,7 +27,7 @@
-
+
{{ds.name}}
@@ -48,7 +48,7 @@
-
+
diff --git a/public/app/features/panel/panel_directive.ts b/public/app/features/panel/panel_directive.ts
index 2497732928b..85431964cd9 100644
--- a/public/app/features/panel/panel_directive.ts
+++ b/public/app/features/panel/panel_directive.ts
@@ -32,7 +32,7 @@ var panelTemplate = `
@@ -12,17 +12,17 @@
Column mappings If your influxdb query returns more than one column you need to specify the column names below. An annotation event is composed of a title, tags, and an additional text field.
Title
-
+
Tags
-
+
Text
-
+
diff --git a/public/app/plugins/panel/graph/axisEditor.html b/public/app/plugins/panel/graph/axisEditor.html
index f7e183a5651..d9b7b4599e0 100644
--- a/public/app/plugins/panel/graph/axisEditor.html
+++ b/public/app/plugins/panel/graph/axisEditor.html
@@ -181,7 +181,7 @@
-
+
Side width
diff --git a/public/app/plugins/sdk.ts b/public/app/plugins/sdk.ts
index a3616903908..854b8777766 100644
--- a/public/app/plugins/sdk.ts
+++ b/public/app/plugins/sdk.ts
@@ -2,6 +2,16 @@ import {PanelCtrl} from 'app/features/panel/panel_ctrl';
import {MetricsPanelCtrl} from 'app/features/panel/metrics_panel_ctrl';
import {QueryCtrl} from 'app/features/panel/query_ctrl';
+import config from 'app/core/config';
+
+export function loadPluginCss(options) {
+ if (config.bootData.user.lightTheme) {
+ System.import(options.light + '!css');
+ } else {
+ System.import(options.dark + '!css');
+ }
+}
+
export {
PanelCtrl,
MetricsPanelCtrl,
diff --git a/public/app/system.conf.js b/public/app/system.conf.js
index 7238bd65ec2..63bbb789f66 100644
--- a/public/app/system.conf.js
+++ b/public/app/system.conf.js
@@ -43,6 +43,8 @@ System.config({
},
map: {
+ text: 'vendor/plugin-text/text.js',
+ css: 'app/core/utils/css_loader.js'
},
meta: {
diff --git a/public/vendor/plugin-css/css.js b/public/vendor/plugin-css/css.js
new file mode 100644
index 00000000000..44839808385
--- /dev/null
+++ b/public/vendor/plugin-css/css.js
@@ -0,0 +1,72 @@
+"use strict";
+
+if (typeof window !== 'undefined') {
+ var waitSeconds = 100;
+
+ var head = document.getElementsByTagName('head')[0];
+
+ // get all link tags in the page
+ var links = document.getElementsByTagName('link');
+ var linkHrefs = [];
+ for (var i = 0; i < links.length; i++) {
+ linkHrefs.push(links[i].href);
+ }
+
+ var isWebkit = !!window.navigator.userAgent.match(/AppleWebKit\/([^ ;]*)/);
+ var webkitLoadCheck = function(link, callback) {
+ setTimeout(function() {
+ for (var i = 0; i < document.styleSheets.length; i++) {
+ var sheet = document.styleSheets[i];
+ if (sheet.href === link.href) {
+ return callback();
+ }
+ }
+ webkitLoadCheck(link, callback);
+ }, 10);
+ };
+
+ var noop = function() {};
+
+ var loadCSS = function(url) {
+ return new Promise(function(resolve, reject) {
+ var timeout = setTimeout(function() {
+ reject('Unable to load CSS');
+ }, waitSeconds * 1000);
+ var _callback = function(error) {
+ clearTimeout(timeout);
+ link.onload = link.onerror = noop;
+ setTimeout(function() {
+ if (error) {
+ reject(error);
+ }
+ else {
+ resolve('');
+ }
+ }, 7);
+ };
+ var link = document.createElement('link');
+ link.type = 'text/css';
+ link.rel = 'stylesheet';
+ link.href = url;
+ if (!isWebkit) {
+ link.onload = function() {
+ _callback();
+ }
+ } else {
+ webkitLoadCheck(link, _callback);
+ }
+ link.onerror = function(event) {
+ _callback(event.error || new Error('Error loading CSS file.'));
+ };
+ head.appendChild(link);
+ });
+ };
+
+ exports.fetch = function(load) {
+ // dont reload styles loaded in the head
+ for (var i = 0; i < linkHrefs.length; i++)
+ if (load.address == linkHrefs[i])
+ return '';
+ return loadCSS(load.address);
+ };
+}
diff --git a/public/vendor/plugin-text/text.js b/public/vendor/plugin-text/text.js
new file mode 100644
index 00000000000..ce23974dddf
--- /dev/null
+++ b/public/vendor/plugin-text/text.js
@@ -0,0 +1,16 @@
+/*
+ Text plugin
+*/
+exports.translate = function(load) {
+ load.metadata.format = 'amd';
+ return 'def' + 'ine(function() {\nreturn "' + load.source
+ .replace(/(["\\])/g, '\\$1')
+ .replace(/[\f]/g, "\\f")
+ .replace(/[\b]/g, "\\b")
+ .replace(/[\n]/g, "\\n")
+ .replace(/[\t]/g, "\\t")
+ .replace(/[\r]/g, "\\r")
+ .replace(/[\u2028]/g, "\\u2028")
+ .replace(/[\u2029]/g, "\\u2029")
+ + '";\n});';
+}
diff --git a/public/views/index.html b/public/views/index.html
index 724afd33f33..e24d4ec6e7d 100644
--- a/public/views/index.html
+++ b/public/views/index.html
@@ -10,14 +10,8 @@
[[if .User.LightTheme]]
- [[ range $css := .PluginCss ]]
-
- [[ end ]]
[[else]]
- [[ range $css := .PluginCss ]]
-
- [[ end ]]
[[end]]
@@ -87,7 +81,7 @@
}];
-
+
diff --git a/tasks/build_task.js b/tasks/build_task.js
index d844b661722..3ace8514164 100644
--- a/tasks/build_task.js
+++ b/tasks/build_task.js
@@ -8,6 +8,7 @@ module.exports = function(grunt) {
'jscs',
'tslint',
'clean:release',
+ 'copy:node_modules',
'copy:public_to_gen',
'typescript:build',
'karma:test',
diff --git a/tasks/default_task.js b/tasks/default_task.js
index 7877fc2a34b..033acb3921e 100644
--- a/tasks/default_task.js
+++ b/tasks/default_task.js
@@ -8,6 +8,7 @@ module.exports = function(grunt) {
'jshint',
'tslint',
'clean:gen',
+ 'copy:node_modules',
'copy:public_to_gen',
'phantomjs',
'css',