Merge branch 'master' into panelbase

This commit is contained in:
Torkel Ödegaard
2016-01-21 15:29:14 +01:00
28 changed files with 258 additions and 161 deletions

View File

@@ -1,6 +1,6 @@
# 3.0.0 (unrelased master branch)
### New Features ###
### New Features
* **Playlists**: Playlists can now be persisted and started from urls, closes [#3655](https://github.com/grafana/grafana/pull/3655)
* **Metadata**: Settings panel now shows dashboard metadata, closes [#3304](https://github.com/grafana/grafana/issues/3304)
* **InfluxDB**: Support for policy selection in query editor, closes [#2018](https://github.com/grafana/grafana/issues/2018)
@@ -10,11 +10,14 @@
**InfluxDB 0.8.x** The data source for the old version of influxdb (0.8.x) is no longer included in default builds. Can easily be installed via improved plugin system, closes #3523
**KairosDB** The data source is no longer included in default builds. Can easily be installed via improved plugin system, closes #3524
### Enhancements ###
### Enhancements
* **Sessions**: Support for memcached as session storage, closes [#3458](https://github.com/grafana/grafana/pull/3458)
* **mysql**: Grafana now supports ssl for mysql, closes [#3584](https://github.com/grafana/grafana/pull/3584)
* **snapshot**: Annotations are now included in snapshots, closes [#3635](https://github.com/grafana/grafana/pull/3635)
### Bug fixes
* **Playlist**: Fix for memory leak when running a playlist, closes [#3794](https://github.com/grafana/grafana/pull/3794)
# 2.6.1 (unrelased, 2.6.x branch)
### New Features

View File

@@ -51,6 +51,8 @@ Name | Description
For details of `metric names` & `label names`, and `label values`, please refer to the [Prometheus documentation](http://prometheus.io/docs/concepts/data_model/#metric-names-and-labels).
> Note: The part of queries is incompatible with the version before 2.6, if you specify like `foo.*`, please change like `metrics(foo.*)`.
You can create a template variable in Grafana and have that variable filled with values from any Prometheus metric exploration query.
You can then use this variable in your Prometheus metric queries.

View File

@@ -1,13 +1,17 @@
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/log"
"github.com/grafana/grafana/pkg/middleware"
m "github.com/grafana/grafana/pkg/models"
@@ -34,32 +38,28 @@ func InitApiPluginRoutes(r *macaron.Macaron) {
handlers = append(handlers, middleware.RoleAuth(m.ROLE_EDITOR, m.ROLE_ADMIN))
}
}
handlers = append(handlers, ApiPlugin(route.Url))
handlers = append(handlers, ApiPlugin(route, plugin.IncludedInAppId))
r.Route(url, route.Method, handlers...)
log.Info("Plugin: Adding route %s", url)
}
}
}
func ApiPlugin(routeUrl string) macaron.Handler {
func ApiPlugin(route *plugins.ApiPluginRoute, includedInAppId string) macaron.Handler {
return func(c *middleware.Context) {
path := c.Params("*")
//Create a HTTP header with the context in it.
ctx, err := json.Marshal(c.SignedInUser)
if err != nil {
c.JsonApiErr(500, "failed to marshal context to json.", err)
return
}
targetUrl, _ := url.Parse(routeUrl)
proxy := NewApiPluginProxy(string(ctx), path, targetUrl)
proxy := NewApiPluginProxy(c, path, route, includedInAppId)
proxy.Transport = dataProxyTransport
proxy.ServeHTTP(c.Resp, c.Req.Request)
}
}
func NewApiPluginProxy(ctx string, proxyPath string, targetUrl *url.URL) *httputil.ReverseProxy {
func NewApiPluginProxy(ctx *middleware.Context, proxyPath string, route *plugins.ApiPluginRoute, includedInAppId 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
@@ -69,7 +69,46 @@ func NewApiPluginProxy(ctx string, proxyPath string, targetUrl *url.URL) *httput
// clear cookie headers
req.Header.Del("Cookie")
req.Header.Del("Set-Cookie")
req.Header.Add("Grafana-Context", ctx)
//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
}
jsonData := make(map[string]interface{})
if includedInAppId != "" {
//lookup appSettings
query := m.GetAppSettingByAppIdQuery{OrgId: ctx.OrgId, AppId: includedInAppId}
if err := bus.Dispatch(&query); err != nil {
ctx.JsonApiErr(500, "failed to get AppSettings of includedAppId.", err)
return
}
jsonData = query.Result.JsonData
}
err = t.Execute(&contentBuf, jsonData)
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}

View File

@@ -31,6 +31,7 @@ func NewAppSettingsDto(def *plugins.AppPlugin, data *models.AppSettings) *AppSet
dto.Enabled = data.Enabled
dto.Pinned = data.Pinned
dto.Info = &def.Info
dto.JsonData = data.JsonData
}
return dto

View File

@@ -1,6 +1,13 @@
package models
import "time"
import (
"errors"
"time"
)
var (
ErrAppSettingNotFound = errors.New("AppSetting not found")
)
type AppSettings struct {
Id int64
@@ -33,3 +40,9 @@ type GetAppSettingsQuery struct {
OrgId int64
Result []*AppSettings
}
type GetAppSettingByAppIdQuery struct {
AppId string
OrgId int64
Result *AppSettings
}

38
pkg/plugins/api_plugin.go Normal file
View File

@@ -0,0 +1,38 @@
package plugins
import (
"encoding/json"
"github.com/grafana/grafana/pkg/models"
)
type ApiPluginRoute 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"`
Headers []ApiPluginHeader `json:"headers"`
}
type ApiPlugin struct {
PluginBase
Routes []*ApiPluginRoute `json:"routes"`
}
type ApiPluginHeader struct {
Name string `json:"name"`
Content string `json:"content"`
}
func (app *ApiPlugin) Load(decoder *json.Decoder, pluginDir string) error {
if err := decoder.Decode(&app); err != nil {
return err
}
app.PluginDir = pluginDir
ApiPlugins[app.Id] = app
return nil
}

View File

@@ -59,6 +59,18 @@ func (app *AppPlugin) Load(decoder *json.Decoder, pluginDir string) error {
}
}
// check if we have child apiPlugins
for _, plugin := range ApiPlugins {
if strings.HasPrefix(plugin.PluginDir, app.PluginDir) {
plugin.IncludedInAppId = app.Id
app.Includes = append(app.Includes, AppIncludeInfo{
Name: plugin.Name,
Id: plugin.Id,
Type: plugin.Type,
})
}
}
Apps[app.Id] = app
return nil
}

View File

@@ -2,8 +2,6 @@ package plugins
import (
"encoding/json"
"github.com/grafana/grafana/pkg/models"
)
type PluginLoader interface {
@@ -44,20 +42,6 @@ type PluginStaticRoute struct {
PluginId string
}
type ApiPluginRoute 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"`
}
type ApiPlugin struct {
PluginBase
Routes []*ApiPluginRoute `json:"routes"`
}
type EnabledPlugins struct {
Panels []*PanelPlugin
DataSources map[string]*DataSourcePlugin

View File

@@ -9,6 +9,7 @@ import (
func init() {
bus.AddHandler("sql", GetAppSettings)
bus.AddHandler("sql", GetAppSettingByAppId)
bus.AddHandler("sql", UpdateAppSettings)
}
@@ -19,6 +20,18 @@ func GetAppSettings(query *m.GetAppSettingsQuery) error {
return sess.Find(&query.Result)
}
func GetAppSettingByAppId(query *m.GetAppSettingByAppIdQuery) error {
appSetting := m.AppSettings{OrgId: query.OrgId, AppId: query.AppId}
has, err := x.Get(&appSetting)
if err != nil {
return err
} else if has == false {
return m.ErrAppSettingNotFound
}
query.Result = &appSetting
return nil
}
func UpdateAppSettings(cmd *m.UpdateAppSettingsCmd) error {
return inTransaction2(func(sess *session) error {
var app m.AppSettings

View File

@@ -0,0 +1,37 @@
///<reference path="../../headers/common.d.ts" />
import _ from 'lodash';
declare var window: any;
export function exportSeriesListToCsv(seriesList) {
var text = 'Series;Time;Value\n';
_.each(seriesList, function(series) {
_.each(series.datapoints, function(dp) {
text += series.alias + ';' + new Date(dp[1]).toISOString() + ';' + dp[0] + '\n';
});
});
saveSaveBlob(text, 'grafana_data_export.csv');
};
export function exportTableDataToCsv(table) {
var text = '';
// add header
_.each(table.columns, function(column) {
text += column.text + ';';
});
text += '\n';
// process data
_.each(table.rows, function(row) {
_.each(row, function(value) {
text += value + ';';
});
text += '\n';
});
saveSaveBlob(text, 'grafana_data_export.csv');
};
export function saveSaveBlob(payload, fname) {
var blob = new Blob([payload], { type: "text/csv;charset=utf-8" });
window.saveAs(blob, fname);
};

View File

@@ -179,17 +179,6 @@ function($, _) {
.replace(/ +/g,'-');
};
kbn.exportSeriesListToCsv = function(seriesList) {
var text = 'Series;Time;Value\n';
_.each(seriesList, function(series) {
_.each(series.datapoints, function(dp) {
text += series.alias + ';' + new Date(dp[1]).toISOString() + ';' + dp[0] + '\n';
});
});
var blob = new Blob([text], { type: "text/csv;charset=utf-8" });
window.saveAs(blob, 'grafana_data_export.csv');
};
kbn.stringToJsRegex = function(str) {
if (str[0] !== '/') {
return new RegExp('^' + str + '$');

View File

@@ -98,6 +98,8 @@
<div class="simple-box-body">
<div ng-if="ctrl.appModel.appId">
<app-config-view app-model="ctrl.appModel"></app-config-view>
<div class="clearfix"></div>
<button type="submit" class="btn btn-success" ng-click="ctrl.update()">Save</button>
</div>
</div>
</section>

View File

@@ -7,6 +7,7 @@ export class SubmenuCtrl {
variables: any;
dashboard: any;
/** @ngInject */
constructor(private $rootScope, private templateValuesSrv, private dynamicDashboardSrv) {
this.annotations = this.dashboard.templating.list;
this.variables = this.dashboard.templating.list;

View File

@@ -159,7 +159,7 @@ function (angular, _) {
};
updateDashLinks();
$rootScope.onAppEvent('dash-links-updated', updateDashLinks, $rootScope);
$rootScope.onAppEvent('dash-links-updated', updateDashLinks, $scope);
});
module.controller('DashLinkEditorCtrl', function($scope, $rootScope) {

View File

@@ -1,5 +0,0 @@
<p class="text-center">Are you sure want to delete "{{playlist.title}}" playlist?</p>
<p class="text-center">
<button type="button" class="btn btn-danger" ng-click="removePlaylist()">Yes</button>
<button type="button" class="btn btn-default" ng-click="dismiss()">No</button>
</p>

View File

@@ -132,11 +132,11 @@ function (angular, config, _) {
};
$scope.movePlaylistItemUp = function(playlistItem) {
$scope.moveDashboard(playlistItem, -1);
$scope.movePlaylistItem(playlistItem, -1);
};
$scope.movePlaylistItemDown = function(playlistItem) {
$scope.moveDashboard(playlistItem, 1);
$scope.movePlaylistItem(playlistItem, 1);
};
$scope.init();

View File

@@ -1,6 +1,7 @@
///<reference path="../../headers/common.d.ts" />
import angular from 'angular';
import config from 'app/core/config';
import coreModule from '../../core/core_module';
import kbn from 'app/core/utils/kbn';
@@ -20,10 +21,9 @@ class PlaylistSrv {
var playedAllDashboards = this.index > this.dashboards.length - 1;
if (playedAllDashboards) {
this.start(this.playlistId);
window.location.href = `${config.appSubUrl}/playlists/play/${this.playlistId}`;
} else {
var dash = this.dashboards[this.index];
this.$location.url('dashboard/' + dash.uri);
this.index++;

View File

@@ -1,6 +1,7 @@
///<reference path="headers/common.d.ts" />
import 'bootstrap';
import 'vendor/filesaver';
import 'lodash-src';
import 'angular-strap';
import 'angular-route';

View File

@@ -1,56 +0,0 @@
define([
'angular',
'./bucket_agg',
'./metric_agg',
],
function (angular) {
'use strict';
var module = angular.module('grafana.directives');
module.directive('metricQueryEditorElasticsearch', function() {
return {controller: 'ElasticQueryCtrl', templateUrl: 'app/plugins/datasource/elasticsearch/partials/query.editor.html'};
});
module.directive('metricQueryOptionsElasticsearch', function() {
return {templateUrl: 'app/plugins/datasource/elasticsearch/partials/query.options.html'};
});
module.directive('annotationsQueryEditorElasticsearch', function() {
return {templateUrl: 'app/plugins/datasource/elasticsearch/partials/annotations.editor.html'};
});
module.directive('elastic', function() {
return {templateUrl: 'app/plugins/datasource/elasticsearch/partials/config.html'};
});
module.directive('elasticMetricAgg', function() {
return {
templateUrl: 'app/plugins/datasource/elasticsearch/partials/metric_agg.html',
controller: 'ElasticMetricAggCtrl',
restrict: 'E',
scope: {
target: "=",
index: "=",
onChange: "&",
getFields: "&",
esVersion: '='
}
};
});
module.directive('elasticBucketAgg', function() {
return {
templateUrl: 'app/plugins/datasource/elasticsearch/partials/bucket_agg.html',
controller: 'ElasticBucketAggCtrl',
restrict: 'E',
scope: {
target: "=",
index: "=",
onChange: "&",
getFields: "&",
}
};
});
});

View File

@@ -5,6 +5,7 @@ import _ from 'lodash';
class MixedDatasource {
/** @ngInject */
constructor(private $q, private datasourceSrv) {
}

View File

@@ -3,13 +3,14 @@ define([
'lodash',
'moment',
'app/core/utils/kbn',
'app/core/utils/file_export',
'app/core/time_series',
'app/features/panel/panel_meta',
'./seriesOverridesCtrl',
'./graph',
'./legend',
],
function (angular, _, moment, kbn, TimeSeries, PanelMeta) {
function (angular, _, moment, kbn, fileExport, TimeSeries, PanelMeta) {
'use strict';
/** @ngInject */
@@ -282,7 +283,7 @@ function (angular, _, moment, kbn, TimeSeries, PanelMeta) {
};
$scope.exportCsv = function() {
kbn.exportSeriesListToCsv($scope.seriesList);
fileExport.exportSeriesListToCsv($scope.seriesList);
};
panelSrv.init($scope);

View File

@@ -3,6 +3,7 @@
import angular from 'angular';
import _ from 'lodash';
import moment from 'moment';
import * as FileExport from 'app/core/utils/file_export';
import PanelMeta from 'app/features/panel/panel_meta2';
import {transformDataToTable} from './transformers';
@@ -22,6 +23,7 @@ export class TablePanelCtrl {
$scope.panelMeta.addEditorTab('Options', 'app/plugins/panel/table/options.html');
$scope.panelMeta.addEditorTab('Time range', 'app/features/panel/partials/panelTime.html');
$scope.panelMeta.addExtendedMenuItem('Export CSV', '', 'exportCsv()');
var panelDefaults = {
targets: [{}],
@@ -124,6 +126,10 @@ export class TablePanelCtrl {
panelHelper.broadcastRender($scope, $scope.table, $scope.dataRaw);
};
$scope.exportCsv = function() {
FileExport.exportTableDataToCsv($scope.table);
};
$scope.init();
}
}

View File

@@ -13,6 +13,7 @@
min-height: 100%;
z-index: 101;
transform: translate3d(-100%, 0, 0);
visibility: hidden;
a:focus {
text-decoration: none;

View File

@@ -56,8 +56,10 @@
</script>
<!-- build:js [[.AppSubUrl]]/public/app/boot.js -->
<script src="[[.AppSubUrl]]/public/vendor/npm/es5-shim/es5-shim.js"></script>
<script src="[[.AppSubUrl]]/public/vendor/npm/es6-shim/es6-shim.js"></script>
<script src="[[.AppSubUrl]]/public/vendor/npm/es6-promise/dist/es6-promise.js"></script>
<script src="[[.AppSubUrl]]/public/vendor/npm/systemjs/dist/system-polyfills.js"></script>
<script src="[[.AppSubUrl]]/public/vendor/npm/systemjs/dist/system.src.js"></script>
<script src="[[.AppSubUrl]]/public/app/system.conf.js"></script>
<script src="[[.AppSubUrl]]/public/app/boot.js"></script>

View File

@@ -1,3 +1,5 @@
#/bin/bash
ln -s .hooks/* .git/hooks/
#ln -s -f .hooks/* .git/hooks/
cd .git/hooks/
cp --symbolic-link -f ../../.hooks/* .

View File

@@ -10,7 +10,7 @@ module.exports = function(grunt) {
'clean:release',
'copy:public_to_gen',
'typescript:build',
// 'karma:test',
'karma:test',
'phantomjs',
'css',
'htmlmin:build',

View File

@@ -28,8 +28,10 @@ module.exports = function(config) {
js: {
src: [
'<%= genDir %>/vendor/npm/es5-shim/es5-shim.js',
'<%= genDir %>/vendor/npm/es6-shim/es6-shim.js',
'<%= genDir %>/vendor/npm/es6-promise/es6-promise.js',
'<%= genDir %>/vendor/npm/es6-promise/dist/es6-promise.js',
'<%= genDir %>/vendor/npm/systemjs/dist/system-polyfills.js',
'<%= genDir %>/vendor/npm/systemjs/dist/system.js',
'<%= genDir %>/app/system.conf.js',
'<%= genDir %>/app/boot.js',

View File

@@ -1,55 +1,63 @@
var page = require('webpage').create();
var args = require('system').args;
var params = {};
var regexp = /^([^=]+)=([^$]+)/;
(function() {
'use strict';
args.forEach(function(arg) {
var parts = arg.match(regexp);
if (!parts) { return; }
params[parts[1]] = parts[2];
});
var page = require('webpage').create();
var args = require('system').args;
var params = {};
var regexp = /^([^=]+)=([^$]+)/;
var usage = "url=<url> png=<filename> width=<width> height=<height> cookiename=<cookiename> sessionid=<sessionid> domain=<domain>";
args.forEach(function(arg) {
var parts = arg.match(regexp);
if (!parts) { return; }
params[parts[1]] = parts[2];
});
if (!params.url || !params.png || !params.cookiename || ! params.sessionid || !params.domain) {
console.log(usage);
phantom.exit();
}
var usage = "url=<url> png=<filename> width=<width> height=<height> cookiename=<cookiename> sessionid=<sessionid> domain=<domain>";
phantom.addCookie({
'name': params.cookiename,
'value': params.sessionid,
'domain': params.domain
});
page.viewportSize = {
width: params.width || '800',
height: params.height || '400'
};
var tries = 0;
page.open(params.url, function (status) {
console.log('Loading a web page: ' + params.url);
function checkIsReady() {
var canvas = page.evaluate(function() {
var body = angular.element(document.body); // 1
var rootScope = body.scope().$root;
var panelsToLoad = angular.element('div.panel').length;
return rootScope.performance.panelsRendered >= panelsToLoad;
});
if (canvas || tries === 1000) {
page.render(params.png);
phantom.exit();
}
else {
tries++;
setTimeout(checkIsReady, 10);
}
if (!params.url || !params.png || !params.cookiename || ! params.sessionid || !params.domain) {
console.log(usage);
phantom.exit();
}
setTimeout(checkIsReady, 200);
phantom.addCookie({
'name': params.cookiename,
'value': params.sessionid,
'domain': params.domain
});
});
page.viewportSize = {
width: params.width || '800',
height: params.height || '400'
};
var tries = 0;
page.open(params.url, function (status) {
console.log('Loading a web page: ' + params.url + ' status: ' + status);
function checkIsReady() {
var canvas = page.evaluate(function() {
if (!window.angular) { return false; }
var body = window.angular.element(document.body); // 1
if (!body.scope) { return false; }
var rootScope = body.scope();
if (!rootScope) {return false;}
if (!rootScope.performance) { return false; }
var panelsToLoad = window.angular.element('div.panel').length;
return rootScope.performance.panelsRendered >= panelsToLoad;
});
if (canvas || tries === 1000) {
page.render(params.png);
phantom.exit();
}
else {
tries++;
setTimeout(checkIsReady, 10);
}
}
setTimeout(checkIsReady, 200);
});
})();