瀏覽代碼

Merge branch 'master' of https://github.com/grafana/grafana into docs-refac

utkarshcmu 9 年之前
父節點
當前提交
cd060c64d5
共有 57 個文件被更改,包括 655 次插入213 次删除
  1. 7 0
      examples/nginx-app/.gitignore
  2. 13 0
      examples/nginx-app/.jscs.json
  3. 36 0
      examples/nginx-app/.jshintrc
  4. 54 0
      examples/nginx-app/Gruntfile.js
  5. 0 20
      examples/nginx-app/module.js
  6. 37 0
      examples/nginx-app/package.json
  7. 0 21
      examples/nginx-app/panel/module.js
  8. 0 1
      examples/nginx-app/partials/config.html
  9. 0 2
      examples/nginx-app/partials/logs.html
  10. 0 1
      examples/nginx-app/partials/stream.html
  11. 3 0
      examples/nginx-app/src/components/config.html
  12. 6 0
      examples/nginx-app/src/components/config.js
  13. 3 0
      examples/nginx-app/src/components/logs.html
  14. 6 0
      examples/nginx-app/src/components/logs.js
  15. 3 0
      examples/nginx-app/src/components/stream.html
  16. 6 0
      examples/nginx-app/src/components/stream.js
  17. 0 0
      examples/nginx-app/src/css/dark.css
  18. 0 0
      examples/nginx-app/src/css/light.css
  19. 17 0
      examples/nginx-app/src/dashboards/dashboard.js
  20. 0 0
      examples/nginx-app/src/img/logo_large.png
  21. 0 0
      examples/nginx-app/src/img/logo_small.png
  22. 9 0
      examples/nginx-app/src/module.js
  23. 15 0
      examples/nginx-app/src/panel/module.js
  24. 0 0
      examples/nginx-app/src/panel/plugin.json
  25. 4 5
      package.json
  26. 10 80
      pkg/api/app_routes.go
  27. 5 0
      pkg/api/cloudwatch/metrics.go
  28. 1 4
      pkg/api/dtos/index.go
  29. 0 4
      pkg/api/index.go
  30. 1 1
      pkg/api/org.go
  31. 99 0
      pkg/api/pluginproxy/pluginproxy.go
  32. 42 0
      pkg/api/pluginproxy/pluginproxy_test.go
  33. 8 0
      pkg/models/app_settings.go
  34. 0 7
      pkg/plugins/app_plugin.go
  35. 2 6
      pkg/services/sqlstore/app_settings.go
  36. 7 2
      pkg/services/sqlstore/sqlstore.go
  37. 1 1
      public/app/core/controllers/json_editor_ctrl.js
  38. 1 0
      public/app/core/routes/routes.ts
  39. 78 0
      public/app/core/utils/css_loader.ts
  40. 1 1
      public/app/features/dashboard/shareSnapshotCtrl.js
  41. 0 36
      public/app/features/datasources/list_ctrl.js
  42. 52 0
      public/app/features/datasources/list_ctrl.ts
  43. 4 4
      public/app/features/datasources/partials/list.html
  44. 1 1
      public/app/features/panel/panel_directive.ts
  45. 2 2
      public/app/features/panel/partials/soloPanel.html
  46. 1 1
      public/app/partials/signup_step2.html
  47. 11 0
      public/app/plugins/datasource/cloudwatch/partials/config.html
  48. 1 1
      public/app/plugins/datasource/graphite/add_graphite_func.js
  49. 4 4
      public/app/plugins/datasource/influxdb/partials/annotations.editor.html
  50. 1 1
      public/app/plugins/panel/graph/axisEditor.html
  51. 10 0
      public/app/plugins/sdk.ts
  52. 2 0
      public/app/system.conf.js
  53. 72 0
      public/vendor/plugin-css/css.js
  54. 16 0
      public/vendor/plugin-text/text.js
  55. 1 7
      public/views/index.html
  56. 1 0
      tasks/build_task.js
  57. 1 0
      tasks/default_task.js

+ 7 - 0
examples/nginx-app/.gitignore

@@ -0,0 +1,7 @@
+.DS_Store
+
+node_modules
+tmp/*
+npm-debug.log
+dist/*
+

+ 13 - 0
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
+}

+ 36 - 0
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
+  }
+}

+ 54 - 0
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']);
+};

+ 0 - 20
examples/nginx-app/module.js

@@ -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,
-  };
-
-});

+ 37 - 0
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"
+}

+ 0 - 21
examples/nginx-app/panel/module.js

@@ -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 = '<h2>nginx!</h2>';
-    NginxPanel.prototype = Object.create(_super.prototype);
-    NginxPanel.prototype.constructor = NginxPanel;
-
-    return NginxPanel;
-  })(sdk.PanelCtrl);
-
-  return {
-    PanelCtrl: NginxPanel
-  };
-});

+ 0 - 1
examples/nginx-app/partials/config.html

@@ -1 +0,0 @@
-<h2>nginx config</h2>

+ 0 - 2
examples/nginx-app/partials/logs.html

@@ -1,2 +0,0 @@
-
-Logs!

+ 0 - 1
examples/nginx-app/partials/stream.html

@@ -1 +0,0 @@
-streams!

+ 3 - 0
examples/nginx-app/src/components/config.html

@@ -0,0 +1,3 @@
+<h3>
+	Nginx config!
+</h3>

+ 6 - 0
examples/nginx-app/src/components/config.js

@@ -0,0 +1,6 @@
+
+export class NginxAppConfigCtrl {
+}
+NginxAppConfigCtrl.templateUrl = 'components/config.html';
+
+

+ 3 - 0
examples/nginx-app/src/components/logs.html

@@ -0,0 +1,3 @@
+<h3>
+	Logs page!
+</h3>

+ 6 - 0
examples/nginx-app/src/components/logs.js

@@ -0,0 +1,6 @@
+
+export class LogsPageCtrl {
+}
+LogsPageCtrl.templateUrl = 'components/logs.html';
+
+

+ 3 - 0
examples/nginx-app/src/components/stream.html

@@ -0,0 +1,3 @@
+<h3>
+	Stream page!
+</h3>

+ 6 - 0
examples/nginx-app/src/components/stream.js

@@ -0,0 +1,6 @@
+
+export class StreamPageCtrl {
+}
+StreamPageCtrl.templateUrl = 'components/stream.html';
+
+

+ 0 - 0
examples/nginx-app/css/dark.css → examples/nginx-app/src/css/dark.css


+ 0 - 0
examples/nginx-app/css/light.css → examples/nginx-app/src/css/light.css


+ 17 - 0
examples/nginx-app/src/dashboards/dashboard.js

@@ -0,0 +1,17 @@
+require([
+], function () {
+
+  function Dashboard() {
+
+    this.getInputs = function() {
+
+    };
+
+    this.buildDashboard = function() {
+
+    };
+  }
+
+  return Dashboard;
+});
+

+ 0 - 0
examples/nginx-app/img/logo_large.png → examples/nginx-app/src/img/logo_large.png


+ 0 - 0
examples/nginx-app/img/logo_small.png → examples/nginx-app/src/img/logo_small.png


+ 9 - 0
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
+};

+ 15 - 0
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 = '<h2>nginx!</h2>';
+
+export {
+  NginxPanelCtrl as PanelCtrl
+};
+

+ 0 - 0
examples/nginx-app/panel/plugin.json → examples/nginx-app/src/panel/plugin.json


+ 4 - 5
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"
   }

+ 10 - 80
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}
-}

+ 5 - 0
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{}{}

+ 1 - 4
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 {

+ 0 - 4
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,

+ 1 - 1
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)
 	}

+ 99 - 0
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}
+}

+ 42 - 0
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")
+		})
+	})
+
+}

+ 8 - 0
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 {

+ 0 - 7
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) {

+ 2 - 6
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

+ 7 - 2
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)

+ 1 - 1
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);

+ 1 - 0
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', {

+ 78 - 0
public/app/core/utils/css_loader.ts

@@ -0,0 +1,78 @@
+///<reference path="../../headers/common.d.ts" />
+
+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);
+}
+

+ 1 - 1
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;
       });
 

+ 0 - 36
public/app/features/datasources/list_ctrl.js

@@ -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();
-
-  });
-});

+ 52 - 0
public/app/features/datasources/list_ctrl.ts

@@ -0,0 +1,52 @@
+///<reference path="../../headers/common.d.ts" />
+
+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);

+ 4 - 4
public/app/features/datasources/partials/list.html

@@ -12,11 +12,11 @@
 		<h1>Data sources</h1>
 		<br>
 
-		<div ng-if="datasources.length === 0">
+		<div ng-if="ctrl.datasources.length === 0">
 			<em>No data sources defined</em>
 		</div>
 
-		<table class="filter-table" ng-if="datasources.length > 0">
+		<table class="filter-table" ng-if="ctrl.datasources.length > 0">
 			<thead>
 				<tr>
 					<th><strong>Name</strong></th>
@@ -27,7 +27,7 @@
 				</tr>
 			</thead>
 			<tbody>
-				<tr ng-repeat="ds in datasources">
+				<tr ng-repeat="ds in ctrl.datasources">
 					<td>
 						<a href="datasources/edit/{{ds.id}}">
 							<i class="fa fa-database"></i> &nbsp; {{ds.name}}
@@ -48,7 +48,7 @@
 						</a>
 					</td>
 					<td class="text-right">
-						<a ng-click="remove(ds)" class="btn btn-danger btn-mini">
+						<a ng-click="ctrl.removeDataSource(ds)" class="btn btn-danger btn-mini">
 							<i class="fa fa-remove"></i>
 						</a>
 					</td>

+ 1 - 1
public/app/features/panel/panel_directive.ts

@@ -32,7 +32,7 @@ var panelTemplate = `
       <div class="gf-box-header">
         <div class="gf-box-title">
           <i ng-class="ctrl.icon"></i>
-          {{ctrl.name}}
+          {{ctrl.pluginName}}
         </div>
 
         <div ng-model="ctrl.editorTabIndex" bs-tabs>

+ 2 - 2
public/app/features/panel/partials/soloPanel.html

@@ -2,8 +2,8 @@
 	<div class="row-fluid">
 		<div class="span12">
 			<div class="panel nospace" ng-if="panel" style="width: 100%">
-				<panel-loader dashboard="dashboard" row="row" panel="panel">
-				</panel-loader>
+				<plugin-component type="panel">
+				</plugin-component>
 			</div>
 		</div>
 </div>

+ 1 - 1
public/app/partials/signup_step2.html

@@ -6,7 +6,7 @@
 	<div class="login-box">
 
 		<div class="login-box-logo">
-			<img src="img/logo_transparent_200x75.png">
+			<img src="public/img/logo_transparent_200x75.png">
 		</div>
 
     <div class="invite-box">

+ 11 - 0
public/app/plugins/datasource/cloudwatch/partials/config.html

@@ -29,5 +29,16 @@
 			</ul>
 			<div class="clearfix"></div>
 		</div>
+		<div class="tight-form">
+			<ul class="tight-form-list">
+				<li class="tight-form-item" style="width: 200px">
+					Custom Metrics namespace<tip>Namespaces of Custom Metrics</tip>
+				</li>
+				<li>
+					<input type="text" class="tight-form-input input-large last" ng-model='ctrl.current.jsonData.customMetricsNamespaces' placeholder="Namespace1,Namespace2"></input>
+				</li>
+			</ul>
+			<div class="clearfix"></div>
+		</div>
 	</div>
 </div>

+ 1 - 1
public/app/plugins/datasource/graphite/add_graphite_func.js

@@ -99,7 +99,7 @@ function (angular, _, $, gfunc) {
         submenu: _.map(list, function(value) {
           return {
             text: value.name,
-            click: "addFunction('" + value.name + "')",
+            click: "ctrl.addFunction('" + value.name + "')",
           };
         })
       };

+ 4 - 4
public/app/plugins/datasource/influxdb/partials/annotations.editor.html

@@ -2,7 +2,7 @@
 	<div class="section">
 		<h5>InfluxDB Query <tip>Example: select text from events where $timeFilter</tip></h5>
 		<div class="editor-option">
-			<input type="text" class="span10" ng-model='annotation.query' placeholder="select text from events where $timeFilter"></input>
+			<input type="text" class="span10" ng-model='ctrl.annotation.query' placeholder="select text from events where $timeFilter"></input>
 		</div>
 	</div>
 </div>
@@ -12,17 +12,17 @@
 		<h5>Column mappings <tip>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.</tip></h5>
 		<div class="editor-option">
 			<label class="small">Title</label>
-			<input type="text" class="input-small" ng-model='annotation.titleColumn' placeholder=""></input>
+			<input type="text" class="input-small" ng-model='ctrl.annotation.titleColumn' placeholder=""></input>
 		</div>
 
 		<div class="editor-option">
 			<label class="small">Tags</label>
-			<input type="text" class="input-small" ng-model='annotation.tagsColumn' placeholder=""></input>
+			<input type="text" class="input-small" ng-model='ctrl.annotation.tagsColumn' placeholder=""></input>
 		</div>
 
 		<div class="editor-option">
 			<label class="small">Text</label>
-			<input type="text" class="input-small" ng-model='annotation.textColumn' placeholder=""></input>
+			<input type="text" class="input-small" ng-model='ctrl.annotation.textColumn' placeholder=""></input>
 		</div>
 	</div>
 </div>

+ 1 - 1
public/app/plugins/panel/graph/axisEditor.html

@@ -181,7 +181,7 @@
 				<li class="tight-form-item">
 					<editor-checkbox text="Right side" model="ctrl.panel.legend.rightSide" change="ctrl.render()"></editor-checkbox>
 				</li>
-				<li ng-if="panel.legend.rightSide" class="tight-form-item">
+				<li ng-if="ctrl.panel.legend.rightSide" class="tight-form-item">
 					Side width
 				</li>
 				<li ng-if="ctrl.panel.legend.rightSide" style="width: 105px">

+ 10 - 0
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,

+ 2 - 0
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: {

+ 72 - 0
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);
+  };
+}

+ 16 - 0
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});';
+}

+ 1 - 7
public/views/index.html

@@ -10,14 +10,8 @@
 
 		[[if .User.LightTheme]]
 		  <link rel="stylesheet" href="[[.AppSubUrl]]/public/css/grafana.light.min.css">
-		  [[ range $css := .PluginCss ]]
-			<link rel="stylesheet" href="[[$.AppSubUrl]]/[[ $css.Light ]]">
-		  [[ end ]]
 		[[else]]
 		  <link rel="stylesheet" href="[[.AppSubUrl]]/public/css/grafana.dark.min.css">
-		  [[ range $css := .PluginCss ]]
-			<link rel="stylesheet" href="[[$.AppSubUrl]]/[[ $css.Dark ]]">
-		  [[ end ]]
 		[[end]]
 
     <link rel="icon" type="image/png" href="[[.AppSubUrl]]/public/img/fav32.png">
@@ -87,7 +81,7 @@
 			}];
 		</script>
 		<!-- Google Tag Manager -->
-		<noscript><iframe src="//www.googletagmanager.com/ns.html?id=GTM-K2MJ65" height="0" width="0" style="display:none;visibility:hidden"></iframe></noscript>
+		<noscript><iframe src="//www.googletagmanager.com/ns.html?id=[[.GoogleTagManagerId]]" height="0" width="0" style="display:none;visibility:hidden"></iframe></noscript>
 		<script>(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start': new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],
 			j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src='//www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);
 		})(window,document,'script','dataLayer','[[.GoogleTagManagerId]]');</script>

+ 1 - 0
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',

+ 1 - 0
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',