Merge branch 'develop' into panel_repeat

This commit is contained in:
Torkel Ödegaard 2015-03-21 15:57:53 -04:00
commit a8d9ec426b
18 changed files with 228 additions and 104 deletions

View File

@ -1,13 +1,14 @@
# 2.0.0 (unreleased)
**New features**
- [Issue #1622](https://github.com/grafana/grafana/issues/1622). Share Panel: The share modal now has an embed option, gives you an iframe that you can use to embedd a single graph on another web site
- [Issue #718](https://github.com/grafana/grafana/issues/718). Dashboard: When saving a dashboard and another user has made changes inbetween the user is promted with a warning if he really wants to overwrite the other's changes
- [Issue #1331](https://github.com/grafana/grafana/issues/1331). Graph & Singlestat: New axis/unit format selector and more units (kbytes, Joule, Watt, eV), and new design for graph axis & grid tab and single stat options tab views
- [Issue #1241](https://github.com/grafana/grafana/issues/1242). Timepicker: New option in timepicker (under dashboard settings), to change ``now`` to be for example ``now-1m``, usefull when you want to ignore last minute because it contains incomplete data
- [Issue #171](https://github.com/grafana/grafana/issues/171). Panel: Different time periods, panels can override dashboard relative time and/or add a time shift
- [Issue #1488](https://github.com/grafana/grafana/issues/1488). Dashboard: Clone dashboard / Save as
- [Issue #1458](https://github.com/grafana/grafana/issues/1458). User: persisted user option for dark or light theme (no longer an option on a dashboard)
- [Issue #452](https://github.com/grafana/grafana/issues/452). Graph: Adds logarithmic scale option (log base 10)
- [Issue #452](https://github.com/grafana/grafana/issues/452). Graph: Adds logarithmic scale option for base 10, base 16 and base 1024
**Enhancements**
- [Issue #1366](https://github.com/grafana/grafana/issues/1366). Graph & Singlestat: Support for additional units, Fahrenheit (°F) and Celsius (°C), Humidity (%H), kW, watt-hour (Wh), kilowatt-hour (kWh), velocities (m/s, km/h, mpg, knot)

View File

@ -12,12 +12,13 @@ import (
func RenderToPng(c *middleware.Context) {
queryReader := util.NewUrlQueryReader(c.Req.URL)
queryParams := fmt.Sprintf("?render=1&%s=%d&%s", middleware.SESS_KEY_USERID, c.UserId, c.Req.URL.RawQuery)
queryParams := fmt.Sprintf("?%s", c.Req.URL.RawQuery)
renderOpts := &renderer.RenderOpts{
Url: c.Params("*") + queryParams,
Width: queryReader.Get("width", "800"),
Height: queryReader.Get("height", "400"),
Url: c.Params("*") + queryParams,
Width: queryReader.Get("width", "800"),
Height: queryReader.Get("height", "400"),
SessionId: c.Session.ID(),
}
renderOpts.Url = setting.ToAbsUrl(renderOpts.Url)

View File

@ -14,9 +14,10 @@ import (
)
type RenderOpts struct {
Url string
Width string
Height string
Url string
Width string
Height string
SessionId string
}
func RenderToPng(params *RenderOpts) (string, error) {
@ -26,7 +27,9 @@ func RenderToPng(params *RenderOpts) (string, error) {
pngPath, _ := filepath.Abs(filepath.Join(setting.ImagesDir, getHash(params.Url)))
pngPath = pngPath + ".png"
cmd := exec.Command(binPath, scriptPath, "url="+params.Url, "width="+params.Width, "height="+params.Height, "png="+pngPath)
cmd := exec.Command(binPath, scriptPath, "url="+params.Url, "width="+params.Width,
"height="+params.Height, "png="+pngPath, "cookiename="+setting.SessionOptions.CookieName,
"domain="+setting.Domain, "sessionid="+params.SessionId)
stdout, err := cmd.StdoutPipe()
if err != nil {

View File

@ -22,13 +22,6 @@ func getRequestUserId(c *Context) int64 {
return userId.(int64)
}
// TODO: figure out a way to secure this
if c.Req.URL.Query().Get("render") == "1" {
userId := c.QueryInt64(SESS_KEY_USERID)
c.Session.Set(SESS_KEY_USERID, userId)
return userId
}
return 0
}

View File

@ -42,7 +42,7 @@ function (angular, _, moment) {
$scope.shareDashboard = function() {
$scope.appEvent('show-modal', {
src: './app/features/dashboard/partials/shareModal.html',
src: './app/features/dashboard/partials/shareDashboard.html',
scope: $scope.$new(),
});
};

View File

@ -0,0 +1,48 @@
<div class="modal-body gf-box gf-box-no-margin" ng-controller="SharePanelCtrl">
<div class="gf-box-header">
<div class="gf-box-title">
<i class="fa fa-share"></i>
Share
</div>
<div ng-model="editor.index" bs-tabs style="text-transform:capitalize;">
<div ng-repeat="tab in ['Link']" data-title="{{tab}}">
</div>
</div>
<button class="gf-box-header-close-btn" ng-click="dismiss();">
<i class="fa fa-remove"></i>
</button>
</div>
<div class="gf-box-body" ng-if="editor.index === 0">
<br>
<div class="gf-form">
<div class="gf-form-row">
<editor-checkbox text="Current time range" model="options.forCurrent" change="buildUrl()"></editor-checkbox>
</div>
</div>
<div class="gf-form">
<div class="gf-form-row">
<editor-checkbox text="Include template variables" model="options.includeTemplateVars" change="buildUrl()"></editor-checkbox>
</div>
</div>
<br>
<div class="gf-form">
<div class="gf-form-row">
<button class="btn btn-inverse pull-right" data-clipboard-text="{{shareUrl}}" clipboard-button><i class="fa fa-clipboard"></i> Copy</button>
<span class="gf-fluid-input">
<input type="text" data-share-panel-url class="input" ng-model='shareUrl'></input>
</span>
</div>
<div>
<div class="editor-row" style="margin-top: 5px;" ng-if="options.toPanel">
<a href="{{imageUrl}}" target="_blank"><i class="fa fa-camera"></i> Direct link rendered image</a>
</div>
</div>
</div>
</div>
</div>

View File

@ -1,53 +0,0 @@
<div class="modal-body gf-box gf-box-no-margin" ng-controller="SharePanelCtrl">
<div class="gf-box-header">
<div class="gf-box-title">
<i class="fa fa-share"></i>
Share
</div>
<div ng-model="editor.index" bs-tabs style="text-transform:capitalize;">
<div ng-repeat="tab in ['Link']" data-title="{{tab}}">
</div>
</div>
<button class="gf-box-header-close-btn" ng-click="dismiss();">
<i class="fa fa-remove"></i>
</button>
</div>
<div class="gf-box-body">
<br>
<div class="gf-form">
<div class="gf-form-row">
<editor-checkbox text="Current time range" model="options.forCurrent" change="buildUrl()"></editor-checkbox>
</div>
</div>
<div class="gf-form" ng-if="panel">
<div class="gf-form-row">
<editor-checkbox text="This panel only" model="options.toPanel" change="buildUrl()"></editor-checkbox>
</div>
</div>
<div class="gf-form">
<div class="gf-form-row">
<editor-checkbox text="Include template variables" model="options.includeTemplateVars" change="buildUrl()"></editor-checkbox>
</div>
</div>
<br>
<div class="gf-form">
<div class="gf-form-row">
<button class="btn btn-inverse pull-right" data-clipboard-text="{{shareUrl}}" clipboard-button><i class="fa fa-clipboard"></i> Copy</button>
<span class="gf-fluid-input">
<input type="text" data-share-panel-url class="input" ng-model='shareUrl'></input>
</span>
</div>
<div>
<div class="editor-row" style="margin-top: 5px;" ng-if="toPanel">
<a href="{{imageUrl}}" target="_blank"><i class="fa fa-camera"></i> Direct link rendered image<a>
</div>
</div>
</div>
</div>

View File

@ -0,0 +1,65 @@
<div class="modal-body gf-box gf-box-no-margin" ng-controller="SharePanelCtrl">
<div class="gf-box-header">
<div class="gf-box-title">
<i class="fa fa-share"></i>
Share Panel
</div>
<div ng-model="editor.index" bs-tabs style="text-transform:capitalize;">
<div ng-repeat="tab in ['Link', 'Embed']" data-title="{{tab}}">
</div>
</div>
<button class="gf-box-header-close-btn" ng-click="dismiss();">
<i class="fa fa-remove"></i>
</button>
</div>
<div class="gf-box-body" ng-if="editor.index === 0">
<br>
<div class="gf-form">
<div class="gf-form-row">
<editor-checkbox text="Current time range" model="options.forCurrent" change="buildUrl()"></editor-checkbox>
</div>
</div>
<div class="gf-form">
<div class="gf-form-row">
<editor-checkbox text="Include template variables" model="options.includeTemplateVars" change="buildUrl()"></editor-checkbox>
</div>
</div>
<br>
<div class="gf-form">
<div class="gf-form-row">
<button class="btn btn-inverse pull-right" data-clipboard-text="{{shareUrl}}" clipboard-button><i class="fa fa-clipboard"></i> Copy</button>
<span class="gf-fluid-input">
<input type="text" data-share-panel-url class="input" ng-model='shareUrl'></input>
</span>
</div>
<div>
<div class="editor-row" style="margin-top: 5px;" ng-if="options.toPanel">
<a href="{{imageUrl}}" target="_blank"><i class="fa fa-camera"></i> Direct link rendered image</a>
</div>
</div>
</div>
</div>
<div class="gf-box-body" ng-if="editor.index === 1">
<h5>IFrame embedding</h5>
<p><em>The html code below can be pasted and included in another web page. Unless anonymous access
is enabled the user viewing that page need to be signed into grafana for the graph to load.
</em>
</p>
<div class="gf-form">
<div class="gf-form-row">
<span class="gf-fluid-input">
<textarea rows="5" data-share-panel-url class="input" ng-model='iframeHtml'></textarea>
</span>
</div>
<button class="btn btn-inverse" data-clipboard-text="{{iframeHtml}}" clipboard-button><i class="fa fa-clipboard"></i> Copy</button>
<br>
<br>
</div>
</div>
</div>

View File

@ -72,8 +72,11 @@ function (angular, _, require, config) {
});
$scope.shareUrl = baseUrl + "?" + paramsArray.join('&');
$scope.imageUrl = $scope.shareUrl.replace('/dashboard/db/', '/render/dashboard/solo/');
$scope.soloUrl = $scope.shareUrl.replace('/dashboard/db/', '/dashboard/solo/');
$scope.iframeHtml = '<iframe src="' + $scope.soloUrl + '" width="450" height="200" frameborder="0"></iframe>';
$scope.imageUrl = $scope.shareUrl.replace('/dashboard/db/', '/render/dashboard/solo/');
$scope.imageUrl += '&width=1000';
$scope.imageUrl += '&height=500';
};

View File

@ -27,7 +27,7 @@ function (angular, _, config) {
$scope.sharePanel = function() {
$scope.appEvent('show-modal', {
src: './app/features/dashboard/partials/shareModal.html',
src: './app/features/dashboard/partials/sharePanel.html',
scope: $scope.$new()
});
};

View File

@ -7,10 +7,22 @@ function (angular, $) {
var module = angular.module('grafana.routes');
module.controller('SoloPanelCtrl', function($scope, backendSrv, $routeParams, dashboardSrv, timeSrv, $location, templateValuesSrv) {
module.controller('SoloPanelCtrl',
function(
$scope,
backendSrv,
$routeParams,
dashboardSrv,
timeSrv,
$location,
templateValuesSrv,
contextSrv) {
var panelId;
$scope.init = function() {
contextSrv.sidemenu = false;
var params = $location.search();
panelId = parseInt(params.panelId);
@ -26,7 +38,7 @@ function (angular, $) {
$scope.dashboard = dashboardSrv.create(dashboard.model);
$scope.row = {
height: $(window).height() + 'px',
height: ($(window).height() - 10) + 'px',
};
$scope.test = "Hej";

View File

@ -2,23 +2,23 @@
<div class="section">
<h5>Drilldown / detail link<tip>These links appear in the dropdown menu in the panel menu. </tip></h5>
<div class="tight-form" ng-repeat="link in panel.links"j>
<div class="tight-form" ng-repeat="link in panel.links">
<ul class="tight-form-list">
<li class="tight-form-item">
<i class="fa fa-remove pointer" ng-click="deleteLink(link)"></i>
</li>
<li class="tight-form-item">title</li>
<li class="tight-form-item" style="width: 80px;">Link title</li>
<li>
<input type="text" ng-model="link.title" class="input-medium tight-form-input">
</li>
<li class="tight-form-item">type</li>
<li class="tight-form-item">Type</li>
<li>
<select class="input-medium tight-form-input" style="width: 101px;" ng-model="link.type" ng-options="f for f in ['dashboard','absolute']"></select>
</li>
<li class="tight-form-item" ng-show="link.type === 'dashboard'">dashboard</li>
<li class="tight-form-item" ng-show="link.type === 'dashboard'">Dashboard</li>
<li ng-show="link.type === 'dashboard'">
<input type="text"
ng-model="link.dashboard"
@ -26,20 +26,30 @@
class="input-large tight-form-input">
</li>
<li class="tight-form-item" ng-show="link.type === 'absolute'">url</li>
<li class="tight-form-item" ng-show="link.type === 'absolute'">Url</li>
<li ng-show="link.type === 'absolute'">
<input type="text" ng-model="link.url" class="input-large tight-form-input">
</li>
<li class="tight-form-item">params
<tip>Use var-variableName=value to pass templating variables.</tip>
</li>
<li>
<input type="text" ng-model="link.params" class="input-medium tight-form-input">
<input type="text" ng-model="link.url" class="input-xlarge tight-form-input">
</li>
</ul>
<div class="clearfix"></div>
</div>
<div class="tight-form">
<ul class="tight-form-list" role="menu">
<li class="tight-form-item">
<i class="fa fa-remove invisible"></i>
</li>
<li class="tight-form-item" style="width: 80px;">
Params
<tip>Use var-variableName=value to pass templating variables.</tip>
</li>
<li>
<input type="text" ng-model="link.params" class="input-xxlarge tight-form-input">
</li>
</ul>
<div class="clearfix"></div>
</div>
</div>
</div>

View File

@ -351,7 +351,7 @@ function (angular, $, kbn, moment, _, GraphTooltip) {
show: scope.panel['y-axis'],
min: scope.panel.grid.leftMin,
index: 1,
logBase: scope.panel.grid.leftLogBase,
logBase: scope.panel.grid.leftLogBase || 1,
max: scope.panel.percentage && scope.panel.stack ? 100 : scope.panel.grid.leftMax,
};
@ -360,7 +360,7 @@ function (angular, $, kbn, moment, _, GraphTooltip) {
if (_.findWhere(data, {yaxis: 2})) {
var secondY = _.clone(defaults);
secondY.index = 2,
secondY.logBase = scope.panel.grid.rightLogBase;
secondY.logBase = scope.panel.grid.rightLogBase || 2,
secondY.position = 'right';
secondY.min = scope.panel.grid.rightMin;
secondY.max = scope.panel.percentage && scope.panel.stack ? 100 : scope.panel.grid.rightMax;
@ -375,7 +375,7 @@ function (angular, $, kbn, moment, _, GraphTooltip) {
}
function applyLogScale(axis, data) {
if (axis.logBase !== 10) {
if (axis.logBase === 1) {
return;
}
@ -391,26 +391,30 @@ function (angular, $, kbn, moment, _, GraphTooltip) {
}
}
}
if (max === null) {
if (max === void 0) {
max = Number.MAX_VALUE;
}
}
axis.min = axis.min !== null ? axis.min : 1;
axis.ticks = [1];
var tick = 1;
axis.min = axis.min !== null ? axis.min : 0;
axis.ticks = [0, 1];
var nextTick = 1;
while (true) {
tick = tick * axis.logBase;
axis.ticks.push(tick);
if (tick > max) {
nextTick = nextTick * axis.logBase;
axis.ticks.push(nextTick);
if (nextTick > max) {
break;
}
}
axis.transform = function(v) { return Math.log(v+0.001); };
axis.inverseTransform = function (v) { return Math.pow(10,v); };
if (axis.logBase === 10) {
axis.transform = function(v) { return Math.log(v+0.1); };
axis.inverseTransform = function (v) { return Math.pow(10,v); };
} else {
axis.transform = function(v) { return Math.log(v+0.1) / Math.log(axis.logBase); };
axis.inverseTransform = function (v) { return Math.pow(axis.logBase,v); };
}
}
function configureAxisMode(axis, format) {

View File

@ -116,7 +116,7 @@ function (angular, app, $, _, kbn, moment, TimeSeries, PanelMeta) {
_.defaults($scope.panel.grid, _d.grid);
_.defaults($scope.panel.legend, _d.legend);
$scope.logScales = {'linear': 1, 'log (base 10)': 10};
$scope.logScales = {'linear': 1, 'log (base 16)': 16, 'log (base 10)': 10, 'log (base 1024)': 1024};
$scope.hiddenSeries = {};
$scope.seriesList = [];

View File

@ -51,5 +51,11 @@ input[type="checkbox"]:checked+label {
height: 100%;
box-sizing: border-box;
}
textarea {
width: 100%;
padding: 5px 6px;
height: 100%;
box-sizing: border-box;
}
}

View File

@ -28,7 +28,7 @@ define([
scope.height = '200px';
scope.panel = {
legend: {},
grid: {},
grid: { },
y_formats: [],
seriesOverrides: [],
tooltip: {
@ -140,6 +140,25 @@ define([
});
});
graphScenario('when logBase is log 10', function(ctx) {
ctx.setup(function(scope) {
scope.panel.grid = {
leftMax: null,
rightMax: null,
leftMin: null,
rightMin: null,
leftLogBase: 10,
};
});
it('should apply axis transform and ticks', function() {
var axis = ctx.plotOptions.yaxes[0];
expect(axis.transform(100)).to.be(Math.log(100+0.0001));
expect(axis.ticks[0]).to.be(1);
expect(axis.ticks[1]).to.be(10);
});
});
graphScenario('should use timeStep for barWidth', function(ctx) {
ctx.setup(function(scope, data) {
scope.panel.bars = true;

View File

@ -10,11 +10,13 @@ define([
var backendSrv = {};
var routeParams = {};
var search = {};
var contextSrv = {};
beforeEach(module('grafana.routes'));
beforeEach(module('grafana.services'));
beforeEach(ctx.providePhase({
$routeParams: routeParams,
contextSrv: contextSrv,
$location: {
search: function() {
return search;
@ -57,6 +59,10 @@ define([
expect(ctx.scope.panel.some).to.be('prop');
});
it('should hide sidemenu', function() {
expect(contextSrv.sidemenu).to.be(false);
});
});
});

View File

@ -9,13 +9,19 @@ args.forEach(function(arg) {
params[parts[1]] = parts[2];
});
var usage = "url=<url> png=<filename> width=<width> height=<height>";
var usage = "url=<url> png=<filename> width=<width> height=<height> cookiename=<cookiename> sessionid=<sessionid> domain=<domain>";
if (!params.url || !params.png) {
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'