Commit 7b4fe824 by Torkel Ödegaard

Merge branch 'master' into panelbase

parents 3e3b9969 28fabade
# 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
......
......@@ -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.
......
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}
......
......@@ -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
......
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
}
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
}
......@@ -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
}
......@@ -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
......
......@@ -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
......
///<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);
};
......@@ -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 + '$');
......
......@@ -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>
......
......@@ -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;
......
......@@ -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) {
......
<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>
......@@ -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();
......
///<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++;
......
///<reference path="headers/common.d.ts" />
import 'bootstrap';
import 'vendor/filesaver';
import 'lodash-src';
import 'angular-strap';
import 'angular-route';
......
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: "&",
}
};
});
});
......@@ -5,6 +5,7 @@ import _ from 'lodash';
class MixedDatasource {
/** @ngInject */
constructor(private $q, private datasourceSrv) {
}
......
......@@ -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);
......
......@@ -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();
}
}
......@@ -13,6 +13,7 @@
min-height: 100%;
z-index: 101;
transform: translate3d(-100%, 0, 0);
visibility: hidden;
a:focus {
text-decoration: none;
......
......@@ -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>
......
#/bin/bash
ln -s .hooks/* .git/hooks/
#ln -s -f .hooks/* .git/hooks/
cd .git/hooks/
cp --symbolic-link -f ../../.hooks/* .
......@@ -10,7 +10,7 @@ module.exports = function(grunt) {
'clean:release',
'copy:public_to_gen',
'typescript:build',
// 'karma:test',
'karma:test',
'phantomjs',
'css',
'htmlmin:build',
......
......@@ -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',
......
var page = require('webpage').create();
var args = require('system').args;
var params = {};
var regexp = /^([^=]+)=([^$]+)/;
args.forEach(function(arg) {
var parts = arg.match(regexp);
if (!parts) { return; }
params[parts[1]] = parts[2];
});
var usage = "url=<url> png=<filename> width=<width> height=<height> cookiename=<cookiename> sessionid=<sessionid> domain=<domain>";
if (!params.url || !params.png || !params.cookiename || ! params.sessionid || !params.domain) {
console.log(usage);
phantom.exit();
}
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);
}
(function() {
'use strict';
var page = require('webpage').create();
var args = require('system').args;
var params = {};
var regexp = /^([^=]+)=([^$]+)/;
args.forEach(function(arg) {
var parts = arg.match(regexp);
if (!parts) { return; }
params[parts[1]] = parts[2];
});
var usage = "url=<url> png=<filename> width=<width> height=<height> cookiename=<cookiename> sessionid=<sessionid> domain=<domain>";
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);
});
})();
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment