mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Merge branch 'master' of https://github.com/grafana/grafana into docs-refac
This commit is contained in:
commit
cd060c64d5
7
examples/nginx-app/.gitignore
vendored
Normal file
7
examples/nginx-app/.gitignore
vendored
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
.DS_Store
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
tmp/*
|
||||||
|
npm-debug.log
|
||||||
|
dist/*
|
||||||
|
|
13
examples/nginx-app/.jscs.json
Normal file
13
examples/nginx-app/.jscs.json
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"disallowImplicitTypeConversion": ["string"],
|
||||||
|
"disallowKeywords": ["with"],
|
||||||
|
"disallowMultipleLineBreaks": true,
|
||||||
|
"disallowMixedSpacesAndTabs": true,
|
||||||
|
"disallowTrailingWhitespace": true,
|
||||||
|
"requireSpacesInFunctionExpression": {
|
||||||
|
"beforeOpeningCurlyBrace": true
|
||||||
|
},
|
||||||
|
"disallowSpacesInsideArrayBrackets": true,
|
||||||
|
"disallowSpacesInsideParentheses": true,
|
||||||
|
"validateIndentation": 2
|
||||||
|
}
|
36
examples/nginx-app/.jshintrc
Normal file
36
examples/nginx-app/.jshintrc
Normal file
@ -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
examples/nginx-app/Gruntfile.js
Normal file
54
examples/nginx-app/Gruntfile.js
Normal file
@ -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']);
|
||||||
|
};
|
@ -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
examples/nginx-app/package.json
Normal file
37
examples/nginx-app/package.json
Normal file
@ -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"
|
||||||
|
}
|
@ -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
|
|
||||||
};
|
|
||||||
});
|
|
@ -1 +0,0 @@
|
|||||||
<h2>nginx config</h2>
|
|
@ -1,2 +0,0 @@
|
|||||||
|
|
||||||
Logs!
|
|
@ -1 +0,0 @@
|
|||||||
streams!
|
|
3
examples/nginx-app/src/components/config.html
Normal file
3
examples/nginx-app/src/components/config.html
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
<h3>
|
||||||
|
Nginx config!
|
||||||
|
</h3>
|
6
examples/nginx-app/src/components/config.js
Normal file
6
examples/nginx-app/src/components/config.js
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
|
||||||
|
export class NginxAppConfigCtrl {
|
||||||
|
}
|
||||||
|
NginxAppConfigCtrl.templateUrl = 'components/config.html';
|
||||||
|
|
||||||
|
|
3
examples/nginx-app/src/components/logs.html
Normal file
3
examples/nginx-app/src/components/logs.html
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
<h3>
|
||||||
|
Logs page!
|
||||||
|
</h3>
|
6
examples/nginx-app/src/components/logs.js
Normal file
6
examples/nginx-app/src/components/logs.js
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
|
||||||
|
export class LogsPageCtrl {
|
||||||
|
}
|
||||||
|
LogsPageCtrl.templateUrl = 'components/logs.html';
|
||||||
|
|
||||||
|
|
3
examples/nginx-app/src/components/stream.html
Normal file
3
examples/nginx-app/src/components/stream.html
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
<h3>
|
||||||
|
Stream page!
|
||||||
|
</h3>
|
6
examples/nginx-app/src/components/stream.js
Normal file
6
examples/nginx-app/src/components/stream.js
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
|
||||||
|
export class StreamPageCtrl {
|
||||||
|
}
|
||||||
|
StreamPageCtrl.templateUrl = 'components/stream.html';
|
||||||
|
|
||||||
|
|
17
examples/nginx-app/src/dashboards/dashboard.js
Normal file
17
examples/nginx-app/src/dashboards/dashboard.js
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
require([
|
||||||
|
], function () {
|
||||||
|
|
||||||
|
function Dashboard() {
|
||||||
|
|
||||||
|
this.getInputs = function() {
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
this.buildDashboard = function() {
|
||||||
|
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return Dashboard;
|
||||||
|
});
|
||||||
|
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB |
Before Width: | Height: | Size: 6.3 KiB After Width: | Height: | Size: 6.3 KiB |
9
examples/nginx-app/src/module.js
Normal file
9
examples/nginx-app/src/module.js
Normal file
@ -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
examples/nginx-app/src/panel/module.js
Normal file
15
examples/nginx-app/src/panel/module.js
Normal file
@ -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
|
||||||
|
};
|
||||||
|
|
@ -50,7 +50,7 @@
|
|||||||
"phantomjs": "^1.9.19",
|
"phantomjs": "^1.9.19",
|
||||||
"reflect-metadata": "0.1.2",
|
"reflect-metadata": "0.1.2",
|
||||||
"rxjs": "5.0.0-beta.0",
|
"rxjs": "5.0.0-beta.0",
|
||||||
"systemjs": "0.19.6",
|
"systemjs": "0.19.20",
|
||||||
"zone.js": "0.5.10"
|
"zone.js": "0.5.10"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
@ -59,8 +59,7 @@
|
|||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "grunt test",
|
"test": "grunt test",
|
||||||
"coveralls": "grunt karma:coveralls && rm -rf ./coverage",
|
"coveralls": "grunt karma:coveralls && rm -rf ./coverage"
|
||||||
"postinstall": "./node_modules/.bin/grunt copy:node_modules"
|
|
||||||
},
|
},
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@ -68,9 +67,9 @@
|
|||||||
"grunt-jscs": "~1.5.x",
|
"grunt-jscs": "~1.5.x",
|
||||||
"grunt-sync": "^0.4.1",
|
"grunt-sync": "^0.4.1",
|
||||||
"karma-sinon": "^1.0.3",
|
"karma-sinon": "^1.0.3",
|
||||||
"lodash": "^2.4.1",
|
"lodash": "^4.0.0",
|
||||||
"sinon": "1.16.1",
|
"sinon": "1.16.1",
|
||||||
"systemjs-builder": "^0.14.15",
|
"systemjs-builder": "^0.15.7",
|
||||||
"tslint": "^3.2.1",
|
"tslint": "^3.2.1",
|
||||||
"typescript": "^1.7.5"
|
"typescript": "^1.7.5"
|
||||||
}
|
}
|
||||||
|
@ -1,17 +1,9 @@
|
|||||||
package api
|
package api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
|
||||||
"net/http/httputil"
|
|
||||||
"net/url"
|
|
||||||
"text/template"
|
|
||||||
|
|
||||||
"gopkg.in/macaron.v1"
|
"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/log"
|
||||||
"github.com/grafana/grafana/pkg/middleware"
|
"github.com/grafana/grafana/pkg/middleware"
|
||||||
m "github.com/grafana/grafana/pkg/models"
|
m "github.com/grafana/grafana/pkg/models"
|
||||||
@ -22,16 +14,14 @@ import (
|
|||||||
func InitAppPluginRoutes(r *macaron.Macaron) {
|
func InitAppPluginRoutes(r *macaron.Macaron) {
|
||||||
for _, plugin := range plugins.Apps {
|
for _, plugin := range plugins.Apps {
|
||||||
for _, route := range plugin.Routes {
|
for _, route := range plugin.Routes {
|
||||||
log.Info("Plugin: Adding proxy route for app plugin")
|
url := util.JoinUrlFragments("/api/plugin-proxy/"+plugin.Id, route.Path)
|
||||||
url := util.JoinUrlFragments("/api/plugin-proxy/", route.Path)
|
|
||||||
handlers := make([]macaron.Handler, 0)
|
handlers := make([]macaron.Handler, 0)
|
||||||
if route.ReqSignedIn {
|
handlers = append(handlers, middleware.Auth(&middleware.AuthOptions{
|
||||||
handlers = append(handlers, middleware.Auth(&middleware.AuthOptions{ReqSignedIn: true}))
|
ReqSignedIn: true,
|
||||||
}
|
ReqGrafanaAdmin: route.ReqGrafanaAdmin,
|
||||||
if route.ReqGrafanaAdmin {
|
}))
|
||||||
handlers = append(handlers, middleware.Auth(&middleware.AuthOptions{ReqSignedIn: true, ReqGrafanaAdmin: true}))
|
|
||||||
}
|
if route.ReqRole != "" {
|
||||||
if route.ReqSignedIn && route.ReqRole != "" {
|
|
||||||
if route.ReqRole == m.ROLE_ADMIN {
|
if route.ReqRole == m.ROLE_ADMIN {
|
||||||
handlers = append(handlers, middleware.RoleAuth(m.ROLE_ADMIN))
|
handlers = append(handlers, middleware.RoleAuth(m.ROLE_ADMIN))
|
||||||
} else if route.ReqRole == m.ROLE_EDITOR {
|
} else if route.ReqRole == m.ROLE_EDITOR {
|
||||||
@ -40,7 +30,7 @@ func InitAppPluginRoutes(r *macaron.Macaron) {
|
|||||||
}
|
}
|
||||||
handlers = append(handlers, AppPluginRoute(route, plugin.Id))
|
handlers = append(handlers, AppPluginRoute(route, plugin.Id))
|
||||||
r.Route(url, route.Method, handlers...)
|
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) {
|
return func(c *middleware.Context) {
|
||||||
path := c.Params("*")
|
path := c.Params("*")
|
||||||
|
|
||||||
proxy := NewApiPluginProxy(c, path, route, appId)
|
proxy := pluginproxy.NewApiPluginProxy(c, path, route, appId)
|
||||||
proxy.Transport = dataProxyTransport
|
proxy.Transport = dataProxyTransport
|
||||||
proxy.ServeHTTP(c.Resp, c.Req.Request)
|
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}
|
|
||||||
}
|
|
||||||
|
@ -126,6 +126,11 @@ func handleGetNamespaces(req *cwRequest, c *middleware.Context) {
|
|||||||
for key := range metricsMap {
|
for key := range metricsMap {
|
||||||
keys = append(keys, key)
|
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))
|
sort.Sort(sort.StringSlice(keys))
|
||||||
|
|
||||||
result := []interface{}{}
|
result := []interface{}{}
|
||||||
|
@ -7,10 +7,7 @@ type IndexViewData struct {
|
|||||||
AppSubUrl string
|
AppSubUrl string
|
||||||
GoogleAnalyticsId string
|
GoogleAnalyticsId string
|
||||||
GoogleTagManagerId string
|
GoogleTagManagerId string
|
||||||
|
MainNavLinks []*NavLink
|
||||||
PluginCss []*PluginCss
|
|
||||||
PluginModules []string
|
|
||||||
MainNavLinks []*NavLink
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type PluginCss struct {
|
type PluginCss struct {
|
||||||
|
@ -81,10 +81,6 @@ func setIndexViewData(c *middleware.Context) (*dtos.IndexViewData, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, plugin := range enabledPlugins.Apps {
|
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 {
|
if plugin.Pinned {
|
||||||
pageLink := &dtos.NavLink{
|
pageLink := &dtos.NavLink{
|
||||||
Text: plugin.Name,
|
Text: plugin.Name,
|
||||||
|
@ -84,7 +84,7 @@ func CreateOrg(c *middleware.Context, cmd m.CreateOrgCommand) Response {
|
|||||||
cmd.UserId = c.UserId
|
cmd.UserId = c.UserId
|
||||||
if err := bus.Dispatch(&cmd); err != nil {
|
if err := bus.Dispatch(&cmd); err != nil {
|
||||||
if err == m.ErrOrgNameTaken {
|
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)
|
return ApiError(500, "Failed to create organization", err)
|
||||||
}
|
}
|
||||||
|
99
pkg/api/pluginproxy/pluginproxy.go
Normal file
99
pkg/api/pluginproxy/pluginproxy.go
Normal file
@ -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
pkg/api/pluginproxy/pluginproxy_test.go
Normal file
42
pkg/api/pluginproxy/pluginproxy_test.go
Normal file
@ -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")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
@ -49,6 +49,14 @@ type UpdateAppSettingsCmd struct {
|
|||||||
OrgId int64 `json:"-"`
|
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
|
// QUERIES
|
||||||
type GetAppSettingsQuery struct {
|
type GetAppSettingsQuery struct {
|
||||||
|
@ -28,7 +28,6 @@ type AppIncludeInfo struct {
|
|||||||
|
|
||||||
type AppPlugin struct {
|
type AppPlugin struct {
|
||||||
FrontendPluginBase
|
FrontendPluginBase
|
||||||
Css *AppPluginCss `json:"css"`
|
|
||||||
Pages []*AppPluginPage `json:"pages"`
|
Pages []*AppPluginPage `json:"pages"`
|
||||||
Routes []*AppPluginRoute `json:"routes"`
|
Routes []*AppPluginRoute `json:"routes"`
|
||||||
Includes []*AppIncludeInfo `json:"-"`
|
Includes []*AppIncludeInfo `json:"-"`
|
||||||
@ -40,7 +39,6 @@ type AppPlugin struct {
|
|||||||
type AppPluginRoute struct {
|
type AppPluginRoute struct {
|
||||||
Path string `json:"path"`
|
Path string `json:"path"`
|
||||||
Method string `json:"method"`
|
Method string `json:"method"`
|
||||||
ReqSignedIn bool `json:"reqSignedIn"`
|
|
||||||
ReqGrafanaAdmin bool `json:"reqGrafanaAdmin"`
|
ReqGrafanaAdmin bool `json:"reqGrafanaAdmin"`
|
||||||
ReqRole models.RoleType `json:"reqRole"`
|
ReqRole models.RoleType `json:"reqRole"`
|
||||||
Url string `json:"url"`
|
Url string `json:"url"`
|
||||||
@ -68,11 +66,6 @@ func (app *AppPlugin) Load(decoder *json.Decoder, pluginDir string) error {
|
|||||||
func (app *AppPlugin) initApp() {
|
func (app *AppPlugin) initApp() {
|
||||||
app.initFrontendPlugin()
|
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
|
// check if we have child panels
|
||||||
for _, panel := range Panels {
|
for _, panel := range Panels {
|
||||||
if strings.HasPrefix(panel.PluginDir, app.PluginDir) {
|
if strings.HasPrefix(panel.PluginDir, app.PluginDir) {
|
||||||
|
@ -42,18 +42,13 @@ func UpdateAppSettings(cmd *m.UpdateAppSettingsCmd) error {
|
|||||||
sess.UseBool("enabled")
|
sess.UseBool("enabled")
|
||||||
sess.UseBool("pinned")
|
sess.UseBool("pinned")
|
||||||
if !exists {
|
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{
|
app = m.AppSettings{
|
||||||
AppId: cmd.AppId,
|
AppId: cmd.AppId,
|
||||||
OrgId: cmd.OrgId,
|
OrgId: cmd.OrgId,
|
||||||
Enabled: cmd.Enabled,
|
Enabled: cmd.Enabled,
|
||||||
Pinned: cmd.Pinned,
|
Pinned: cmd.Pinned,
|
||||||
JsonData: cmd.JsonData,
|
JsonData: cmd.JsonData,
|
||||||
SecureJsonData: secureJsonData,
|
SecureJsonData: cmd.GetEncryptedJsonData(),
|
||||||
Created: time.Now(),
|
Created: time.Now(),
|
||||||
Updated: time.Now(),
|
Updated: time.Now(),
|
||||||
}
|
}
|
||||||
@ -63,6 +58,7 @@ func UpdateAppSettings(cmd *m.UpdateAppSettingsCmd) error {
|
|||||||
for key, data := range cmd.SecureJsonData {
|
for key, data := range cmd.SecureJsonData {
|
||||||
app.SecureJsonData[key] = util.Encrypt([]byte(data), setting.SecretKey)
|
app.SecureJsonData[key] = util.Encrypt([]byte(data), setting.SecretKey)
|
||||||
}
|
}
|
||||||
|
app.SecureJsonData = cmd.GetEncryptedJsonData()
|
||||||
app.Updated = time.Now()
|
app.Updated = time.Now()
|
||||||
app.Enabled = cmd.Enabled
|
app.Enabled = cmd.Enabled
|
||||||
app.JsonData = cmd.JsonData
|
app.JsonData = cmd.JsonData
|
||||||
|
@ -149,8 +149,13 @@ func getEngine() (*xorm.Engine, error) {
|
|||||||
if len(fields) > 1 && len(strings.TrimSpace(fields[1])) > 0 {
|
if len(fields) > 1 && len(strings.TrimSpace(fields[1])) > 0 {
|
||||||
port = fields[1]
|
port = fields[1]
|
||||||
}
|
}
|
||||||
cnnstr = fmt.Sprintf("user=%s password=%s host=%s port=%s dbname=%s sslmode=%s",
|
if DbCfg.Pwd == "" {
|
||||||
DbCfg.User, DbCfg.Pwd, host, port, DbCfg.Name, DbCfg.SslMode)
|
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":
|
case "sqlite3":
|
||||||
if !filepath.IsAbs(DbCfg.Path) {
|
if !filepath.IsAbs(DbCfg.Path) {
|
||||||
DbCfg.Path = filepath.Join(setting.DataPath, DbCfg.Path)
|
DbCfg.Path = filepath.Join(setting.DataPath, DbCfg.Path)
|
||||||
|
@ -8,7 +8,7 @@ function (angular, coreModule) {
|
|||||||
coreModule.default.controller('JsonEditorCtrl', function($scope) {
|
coreModule.default.controller('JsonEditorCtrl', function($scope) {
|
||||||
|
|
||||||
$scope.json = angular.toJson($scope.object, true);
|
$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 () {
|
$scope.update = function () {
|
||||||
var newObject = angular.fromJson($scope.json);
|
var newObject = angular.fromJson($scope.json);
|
||||||
|
@ -45,6 +45,7 @@ function setupAngularRoutes($routeProvider, $locationProvider) {
|
|||||||
.when('/datasources', {
|
.when('/datasources', {
|
||||||
templateUrl: 'public/app/features/datasources/partials/list.html',
|
templateUrl: 'public/app/features/datasources/partials/list.html',
|
||||||
controller : 'DataSourcesCtrl',
|
controller : 'DataSourcesCtrl',
|
||||||
|
controllerAs: 'ctrl',
|
||||||
resolve: loadOrgBundle,
|
resolve: loadOrgBundle,
|
||||||
})
|
})
|
||||||
.when('/datasources/edit/:id', {
|
.when('/datasources/edit/:id', {
|
||||||
|
78
public/app/core/utils/css_loader.ts
Normal file
78
public/app/core/utils/css_loader.ts
Normal file
@ -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);
|
||||||
|
}
|
||||||
|
|
@ -117,7 +117,7 @@ function (angular, _) {
|
|||||||
// remove template queries
|
// remove template queries
|
||||||
_.each(dash.templating.list, function(variable) {
|
_.each(dash.templating.list, function(variable) {
|
||||||
variable.query = "";
|
variable.query = "";
|
||||||
variable.options = [];
|
variable.options = variable.current;
|
||||||
variable.refresh = false;
|
variable.refresh = false;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -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
public/app/features/datasources/list_ctrl.ts
Normal file
52
public/app/features/datasources/list_ctrl.ts
Normal file
@ -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);
|
@ -12,11 +12,11 @@
|
|||||||
<h1>Data sources</h1>
|
<h1>Data sources</h1>
|
||||||
<br>
|
<br>
|
||||||
|
|
||||||
<div ng-if="datasources.length === 0">
|
<div ng-if="ctrl.datasources.length === 0">
|
||||||
<em>No data sources defined</em>
|
<em>No data sources defined</em>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<table class="filter-table" ng-if="datasources.length > 0">
|
<table class="filter-table" ng-if="ctrl.datasources.length > 0">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th><strong>Name</strong></th>
|
<th><strong>Name</strong></th>
|
||||||
@ -27,7 +27,7 @@
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr ng-repeat="ds in datasources">
|
<tr ng-repeat="ds in ctrl.datasources">
|
||||||
<td>
|
<td>
|
||||||
<a href="datasources/edit/{{ds.id}}">
|
<a href="datasources/edit/{{ds.id}}">
|
||||||
<i class="fa fa-database"></i> {{ds.name}}
|
<i class="fa fa-database"></i> {{ds.name}}
|
||||||
@ -48,7 +48,7 @@
|
|||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
<td class="text-right">
|
<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>
|
<i class="fa fa-remove"></i>
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
|
@ -32,7 +32,7 @@ var panelTemplate = `
|
|||||||
<div class="gf-box-header">
|
<div class="gf-box-header">
|
||||||
<div class="gf-box-title">
|
<div class="gf-box-title">
|
||||||
<i ng-class="ctrl.icon"></i>
|
<i ng-class="ctrl.icon"></i>
|
||||||
{{ctrl.name}}
|
{{ctrl.pluginName}}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div ng-model="ctrl.editorTabIndex" bs-tabs>
|
<div ng-model="ctrl.editorTabIndex" bs-tabs>
|
||||||
|
@ -2,8 +2,8 @@
|
|||||||
<div class="row-fluid">
|
<div class="row-fluid">
|
||||||
<div class="span12">
|
<div class="span12">
|
||||||
<div class="panel nospace" ng-if="panel" style="width: 100%">
|
<div class="panel nospace" ng-if="panel" style="width: 100%">
|
||||||
<panel-loader dashboard="dashboard" row="row" panel="panel">
|
<plugin-component type="panel">
|
||||||
</panel-loader>
|
</plugin-component>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -6,7 +6,7 @@
|
|||||||
<div class="login-box">
|
<div class="login-box">
|
||||||
|
|
||||||
<div class="login-box-logo">
|
<div class="login-box-logo">
|
||||||
<img src="img/logo_transparent_200x75.png">
|
<img src="public/img/logo_transparent_200x75.png">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="invite-box">
|
<div class="invite-box">
|
||||||
|
@ -29,5 +29,16 @@
|
|||||||
</ul>
|
</ul>
|
||||||
<div class="clearfix"></div>
|
<div class="clearfix"></div>
|
||||||
</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>
|
||||||
</div>
|
</div>
|
||||||
|
@ -99,7 +99,7 @@ function (angular, _, $, gfunc) {
|
|||||||
submenu: _.map(list, function(value) {
|
submenu: _.map(list, function(value) {
|
||||||
return {
|
return {
|
||||||
text: value.name,
|
text: value.name,
|
||||||
click: "addFunction('" + value.name + "')",
|
click: "ctrl.addFunction('" + value.name + "')",
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
};
|
};
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
<div class="section">
|
<div class="section">
|
||||||
<h5>InfluxDB Query <tip>Example: select text from events where $timeFilter</tip></h5>
|
<h5>InfluxDB Query <tip>Example: select text from events where $timeFilter</tip></h5>
|
||||||
<div class="editor-option">
|
<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>
|
</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>
|
<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">
|
<div class="editor-option">
|
||||||
<label class="small">Title</label>
|
<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>
|
||||||
|
|
||||||
<div class="editor-option">
|
<div class="editor-option">
|
||||||
<label class="small">Tags</label>
|
<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>
|
||||||
|
|
||||||
<div class="editor-option">
|
<div class="editor-option">
|
||||||
<label class="small">Text</label>
|
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -181,7 +181,7 @@
|
|||||||
<li class="tight-form-item">
|
<li class="tight-form-item">
|
||||||
<editor-checkbox text="Right side" model="ctrl.panel.legend.rightSide" change="ctrl.render()"></editor-checkbox>
|
<editor-checkbox text="Right side" model="ctrl.panel.legend.rightSide" change="ctrl.render()"></editor-checkbox>
|
||||||
</li>
|
</li>
|
||||||
<li ng-if="panel.legend.rightSide" class="tight-form-item">
|
<li ng-if="ctrl.panel.legend.rightSide" class="tight-form-item">
|
||||||
Side width
|
Side width
|
||||||
</li>
|
</li>
|
||||||
<li ng-if="ctrl.panel.legend.rightSide" style="width: 105px">
|
<li ng-if="ctrl.panel.legend.rightSide" style="width: 105px">
|
||||||
|
@ -2,6 +2,16 @@ import {PanelCtrl} from 'app/features/panel/panel_ctrl';
|
|||||||
import {MetricsPanelCtrl} from 'app/features/panel/metrics_panel_ctrl';
|
import {MetricsPanelCtrl} from 'app/features/panel/metrics_panel_ctrl';
|
||||||
import {QueryCtrl} from 'app/features/panel/query_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 {
|
export {
|
||||||
PanelCtrl,
|
PanelCtrl,
|
||||||
MetricsPanelCtrl,
|
MetricsPanelCtrl,
|
||||||
|
@ -43,6 +43,8 @@ System.config({
|
|||||||
},
|
},
|
||||||
|
|
||||||
map: {
|
map: {
|
||||||
|
text: 'vendor/plugin-text/text.js',
|
||||||
|
css: 'app/core/utils/css_loader.js'
|
||||||
},
|
},
|
||||||
|
|
||||||
meta: {
|
meta: {
|
||||||
|
72
public/vendor/plugin-css/css.js
vendored
Normal file
72
public/vendor/plugin-css/css.js
vendored
Normal file
@ -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
public/vendor/plugin-text/text.js
vendored
Normal file
16
public/vendor/plugin-text/text.js
vendored
Normal file
@ -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});';
|
||||||
|
}
|
@ -10,14 +10,8 @@
|
|||||||
|
|
||||||
[[if .User.LightTheme]]
|
[[if .User.LightTheme]]
|
||||||
<link rel="stylesheet" href="[[.AppSubUrl]]/public/css/grafana.light.min.css">
|
<link rel="stylesheet" href="[[.AppSubUrl]]/public/css/grafana.light.min.css">
|
||||||
[[ range $css := .PluginCss ]]
|
|
||||||
<link rel="stylesheet" href="[[$.AppSubUrl]]/[[ $css.Light ]]">
|
|
||||||
[[ end ]]
|
|
||||||
[[else]]
|
[[else]]
|
||||||
<link rel="stylesheet" href="[[.AppSubUrl]]/public/css/grafana.dark.min.css">
|
<link rel="stylesheet" href="[[.AppSubUrl]]/public/css/grafana.dark.min.css">
|
||||||
[[ range $css := .PluginCss ]]
|
|
||||||
<link rel="stylesheet" href="[[$.AppSubUrl]]/[[ $css.Dark ]]">
|
|
||||||
[[ end ]]
|
|
||||||
[[end]]
|
[[end]]
|
||||||
|
|
||||||
<link rel="icon" type="image/png" href="[[.AppSubUrl]]/public/img/fav32.png">
|
<link rel="icon" type="image/png" href="[[.AppSubUrl]]/public/img/fav32.png">
|
||||||
@ -87,7 +81,7 @@
|
|||||||
}];
|
}];
|
||||||
</script>
|
</script>
|
||||||
<!-- Google Tag Manager -->
|
<!-- 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],
|
<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);
|
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>
|
})(window,document,'script','dataLayer','[[.GoogleTagManagerId]]');</script>
|
||||||
|
@ -8,6 +8,7 @@ module.exports = function(grunt) {
|
|||||||
'jscs',
|
'jscs',
|
||||||
'tslint',
|
'tslint',
|
||||||
'clean:release',
|
'clean:release',
|
||||||
|
'copy:node_modules',
|
||||||
'copy:public_to_gen',
|
'copy:public_to_gen',
|
||||||
'typescript:build',
|
'typescript:build',
|
||||||
'karma:test',
|
'karma:test',
|
||||||
|
@ -8,6 +8,7 @@ module.exports = function(grunt) {
|
|||||||
'jshint',
|
'jshint',
|
||||||
'tslint',
|
'tslint',
|
||||||
'clean:gen',
|
'clean:gen',
|
||||||
|
'copy:node_modules',
|
||||||
'copy:public_to_gen',
|
'copy:public_to_gen',
|
||||||
'phantomjs',
|
'phantomjs',
|
||||||
'css',
|
'css',
|
||||||
|
Loading…
Reference in New Issue
Block a user