mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Renamed src directory to public
This commit is contained in:
57
public/app/panels/dashlist/editor.html
Normal file
57
public/app/panels/dashlist/editor.html
Normal file
@@ -0,0 +1,57 @@
|
||||
|
||||
<div class="editor-row">
|
||||
<div class="section" style="margin-bottom: 20px">
|
||||
<div class="tight-form">
|
||||
<ul class="tight-form-list">
|
||||
<li class="tight-form-item" style="width: 110px">
|
||||
<strong>Mode</strong>
|
||||
</li>
|
||||
<li>
|
||||
<select class="input-small tight-form-input" ng-model="panel.mode" ng-options="f for f in modes" ng-change="get_data()"></select>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section" style="margin-bottom: 20px" ng-if="panel.mode === 'search'">
|
||||
<div class="tight-form">
|
||||
<ul class="tight-form-list">
|
||||
<li class="tight-form-item" style="width: 110px">
|
||||
<strong>Search options</strong>
|
||||
</li>
|
||||
<li class="tight-form-item">
|
||||
Query
|
||||
</li>
|
||||
<li>
|
||||
<input type="text" class="input-small tight-form-input" placeholder="title query"
|
||||
ng-model="panel.query" ng-change="get_data()" ng-model-onblur>
|
||||
</li>
|
||||
<li class="tight-form-item">
|
||||
Tag
|
||||
</li>
|
||||
<li>
|
||||
<input type="text" class="input-small tight-form-input" placeholder="full tag name"
|
||||
ng-model="panel.tag" ng-change="get_data()" ng-model-onblur>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="editor-row">
|
||||
<div class="section" style="margin-bottom: 20px">
|
||||
<div class="tight-form">
|
||||
<ul class="tight-form-list">
|
||||
<li class="tight-form-item" style="width: 110px">
|
||||
<strong>Limit number to</strong>
|
||||
</li>
|
||||
<li>
|
||||
<input class="input-small tight-form-input" type="number" ng-model="panel.limit" ng-model-onblur ng-change="get_data">
|
||||
</li>
|
||||
</ul>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
13
public/app/panels/dashlist/module.html
Normal file
13
public/app/panels/dashlist/module.html
Normal file
@@ -0,0 +1,13 @@
|
||||
<grafana-panel>
|
||||
<div class="dashlist">
|
||||
<div class="dashlist-item" ng-repeat="dash in dashList">
|
||||
<a class="dashlist-link" href="dashboard/db/{{dash.slug}}">
|
||||
<span class="dashlist-title">
|
||||
{{dash.title}}
|
||||
</span>
|
||||
<span class="dashlist-star">
|
||||
<i class="fa" ng-class="{'fa-star': dash.isStarred, 'fa-star-o': !dash.isStarred}"></i>
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
</grafana-panel>
|
||||
71
public/app/panels/dashlist/module.js
Normal file
71
public/app/panels/dashlist/module.js
Normal file
@@ -0,0 +1,71 @@
|
||||
define([
|
||||
'angular',
|
||||
'app',
|
||||
'lodash',
|
||||
'config',
|
||||
'components/panelmeta',
|
||||
],
|
||||
function (angular, app, _, config, PanelMeta) {
|
||||
'use strict';
|
||||
|
||||
var module = angular.module('grafana.panels.dashlist', []);
|
||||
app.useModule(module);
|
||||
|
||||
module.directive('grafanaPanelDashlist', function() {
|
||||
return {
|
||||
controller: 'DashListPanelCtrl',
|
||||
templateUrl: 'app/panels/dashlist/module.html',
|
||||
};
|
||||
});
|
||||
|
||||
module.controller('DashListPanelCtrl', function($scope, panelSrv, backendSrv) {
|
||||
|
||||
$scope.panelMeta = new PanelMeta({
|
||||
panelName: 'Dash list',
|
||||
editIcon: "fa fa-star",
|
||||
fullscreen: true,
|
||||
});
|
||||
|
||||
$scope.panelMeta.addEditorTab('Options', 'app/panels/dashlist/editor.html');
|
||||
|
||||
var defaults = {
|
||||
mode: 'starred',
|
||||
query: '',
|
||||
limit: 10,
|
||||
tag: '',
|
||||
};
|
||||
|
||||
$scope.modes = ['starred', 'search'];
|
||||
|
||||
_.defaults($scope.panel, defaults);
|
||||
|
||||
$scope.dashList = [];
|
||||
|
||||
$scope.init = function() {
|
||||
panelSrv.init($scope);
|
||||
|
||||
if ($scope.isNewPanel()) {
|
||||
$scope.panel.title = "Starred Dashboards";
|
||||
}
|
||||
};
|
||||
|
||||
$scope.refreshData = function() {
|
||||
var params = {
|
||||
limit: $scope.panel.limit
|
||||
};
|
||||
|
||||
if ($scope.panel.mode === 'starred') {
|
||||
params.starred = "true";
|
||||
} else {
|
||||
params.query = $scope.panel.query;
|
||||
params.tag = $scope.panel.tag;
|
||||
}
|
||||
|
||||
return backendSrv.search(params).then(function(result) {
|
||||
$scope.dashList = result.dashboards;
|
||||
});
|
||||
};
|
||||
|
||||
$scope.init();
|
||||
});
|
||||
});
|
||||
245
public/app/panels/graph/axisEditor.html
Normal file
245
public/app/panels/graph/axisEditor.html
Normal file
@@ -0,0 +1,245 @@
|
||||
|
||||
<div class="editor-row">
|
||||
<div class="section" style="margin-bottom: 20px">
|
||||
<div class="tight-form">
|
||||
<ul class="tight-form-list">
|
||||
<li class="tight-form-item" style="width: 80px">
|
||||
<strong>Left Y</strong>
|
||||
</li>
|
||||
<li class="tight-form-item">
|
||||
Unit
|
||||
</li>
|
||||
<li class="dropdown" style="width: 140px;"
|
||||
ng-model="panel.y_formats[0]"
|
||||
dropdown-typeahead="unitFormats"
|
||||
dropdown-typeahead-on-select="setUnitFormat(0, $subItem)">
|
||||
</li>
|
||||
<li class="tight-form-item">
|
||||
Grid Max
|
||||
</li>
|
||||
<li>
|
||||
<input type="number" class="input-small tight-form-input" placeholder="auto"
|
||||
empty-to-null ng-model="panel.grid.leftMax"
|
||||
ng-change="render()" ng-model-onblur>
|
||||
</li>
|
||||
<li class="tight-form-item">
|
||||
Min
|
||||
</li>
|
||||
<li>
|
||||
<input type="number" class="input-small tight-form-input" placeholder="auto"
|
||||
empty-to-null ng-model="panel.grid.leftMin"
|
||||
ng-change="render()" ng-model-onblur>
|
||||
</li>
|
||||
<li class="tight-form-item">
|
||||
Scale type
|
||||
</li>
|
||||
<li>
|
||||
<select class="input-small tight-form-input" style="width: 113px" ng-model="panel.grid.leftLogBase" ng-options="v as k for (k, v) in logScales" ng-change="render()"></select>
|
||||
</li>
|
||||
<li class="tight-form-item">
|
||||
Label
|
||||
</li>
|
||||
<li>
|
||||
<input type="text" class="input-small tight-form-input last"
|
||||
ng-model="panel.leftYAxisLabel" ng-change="render()" ng-model-onblur>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
<div class="tight-form">
|
||||
<ul class="tight-form-list">
|
||||
<li class="tight-form-item" style="width: 80px">
|
||||
<strong>Right Y</strong>
|
||||
</li>
|
||||
<li class="tight-form-item">
|
||||
Unit
|
||||
</li>
|
||||
<li class="dropdown" style="width: 140px"
|
||||
ng-model="panel.y_formats[1]"
|
||||
dropdown-typeahead="unitFormats"
|
||||
dropdown-typeahead-on-select="setUnitFormat(1, $subItem)">
|
||||
</li>
|
||||
<li class="tight-form-item">
|
||||
Grid Max
|
||||
</li>
|
||||
<li>
|
||||
<input type="number" class="input-small tight-form-input" placeholder="auto"
|
||||
empty-to-null ng-model="panel.grid.rightMax"
|
||||
ng-change="render()" ng-model-onblur>
|
||||
</li>
|
||||
<li class="tight-form-item">
|
||||
Min
|
||||
</li>
|
||||
<li>
|
||||
<input type="number" class="input-small tight-form-input" placeholder="auto"
|
||||
empty-to-null ng-model="panel.grid.rightMin"
|
||||
ng-change="render()" ng-model-onblur>
|
||||
</li>
|
||||
<li class="tight-form-item">
|
||||
Scale type
|
||||
</li>
|
||||
<li>
|
||||
<select class="input-small tight-form-input" style="width: 113px" ng-model="panel.grid.rightLogBase" ng-options="v as k for (k, v) in logScales" ng-change="render()"></select>
|
||||
</li>
|
||||
<li class="tight-form-item">
|
||||
Label
|
||||
</li>
|
||||
<li>
|
||||
<input type="text" class="input-small tight-form-input last"
|
||||
ng-model="panel.rightYAxisLabel" ng-change="render()" ng-model-onblur>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section" style="margin-bottom: 20px">
|
||||
<div class="tight-form">
|
||||
<ul class="tight-form-list">
|
||||
<li class="tight-form-item" style="width: 80px">
|
||||
<strong>Show Axis</strong>
|
||||
</li>
|
||||
<li class="tight-form-item">
|
||||
X-Axis
|
||||
<input class="cr1" id="hideXAxis" type="checkbox"
|
||||
ng-model="panel['x-axis']" ng-checked="panel['x-axis']" ng-change="render()">
|
||||
<label for="hideXAxis" class="cr1"></label>
|
||||
</li>
|
||||
<li class="tight-form-item last">
|
||||
Y-Axis
|
||||
<input class="cr1" id="hideYAxis" type="checkbox"
|
||||
ng-model="panel['y-axis']" ng-checked="panel['y-axis']" ng-change="render()">
|
||||
<label for="hideYAxis" class="cr1"></label>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
<div class="tight-form">
|
||||
<ul class="tight-form-list">
|
||||
<li class="tight-form-item" style="width: 80px">
|
||||
<strong>Thresholds</strong>
|
||||
</li>
|
||||
<li class="tight-form-item">
|
||||
Level 1
|
||||
</li>
|
||||
<li>
|
||||
<input type="number" class="input-small tight-form-input"
|
||||
ng-model="panel.grid.threshold1" ng-change="render()" ng-model-onblur>
|
||||
</li>
|
||||
<li class="tight-form-item">
|
||||
<spectrum-picker ng-model="panel.grid.threshold1Color" ng-change="render()" ></spectrum-picker>
|
||||
</li>
|
||||
<li class="tight-form-item">
|
||||
Level 2
|
||||
</li>
|
||||
<li>
|
||||
<input type="number" class="input-small tight-form-input"
|
||||
ng-model="panel.grid.threshold2" ng-change="render()" ng-model-onblur>
|
||||
</li>
|
||||
<li class="tight-form-item">
|
||||
<spectrum-picker ng-model="panel.grid.threshold2Color" ng-change="render()" ></spectrum-picker>
|
||||
</li>
|
||||
<li class="tight-form-item last">
|
||||
Line mode
|
||||
<input class="cr1" id="panel.grid.thresholdLine" type="checkbox"
|
||||
ng-model="panel.grid.thresholdLine" ng-checked="panel.grid.thresholdLine" ng-change="render()">
|
||||
<label for="panel.grid.thresholdLine" class="cr1"></label>
|
||||
|
||||
</li>
|
||||
</ul>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="editor-row">
|
||||
<div class="section">
|
||||
<div class="tight-form last">
|
||||
<ul class="tight-form-list">
|
||||
<li class="tight-form-item" style="width: 110px">
|
||||
<strong>Legend</strong>
|
||||
</li>
|
||||
<li class="tight-form-item">
|
||||
Show
|
||||
<input class="cr1" id="panel.legend.show" type="checkbox"
|
||||
ng-model="panel.legend.show" ng-checked="panel.legend.show" ng-change="get_data()">
|
||||
<label for="panel.legend.show" class="cr1"></label>
|
||||
</li>
|
||||
<li class="tight-form-item">
|
||||
Table
|
||||
<input class="cr1" id="panel.legend.alignAsTable" type="checkbox"
|
||||
ng-model="panel.legend.alignAsTable" ng-checked="panel.legend.alignAsTable" ng-change="render()">
|
||||
<label for="panel.legend.alignAsTable" class="cr1"></label>
|
||||
</li>
|
||||
<li class="tight-form-item">
|
||||
Right side
|
||||
<input class="cr1" id="panel.legend.rightSide" type="checkbox"
|
||||
ng-model="panel.legend.rightSide" ng-checked="panel.legend.rightSide" ng-change="render()">
|
||||
<label for="panel.legend.rightSide" class="cr1"></label>
|
||||
</li>
|
||||
<li class="tight-form-item last">
|
||||
<span bs-tooltip="'Hides series with only null values'">Hide empty <span>
|
||||
<input class="cr1" id="panel.legend.hideEmpty" type="checkbox"
|
||||
ng-model="panel.legend.hideEmpty" ng-checked="panel.legend.hideEmpty" ng-change="render()">
|
||||
<label for="panel.legend.hideEmpty" class="cr1"></label>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section">
|
||||
<div class="tight-form">
|
||||
<ul class="tight-form-list">
|
||||
<li class="tight-form-item" style="width: 100px">
|
||||
<strong>Legend values</strong>
|
||||
</li>
|
||||
<li class="tight-form-item">
|
||||
Min
|
||||
<input class="cr1" id="panel.legend.min" type="checkbox"
|
||||
ng-model="panel.legend.min" ng-checked="panel.legend.min" ng-change="legendValuesOptionChanged()">
|
||||
<label for="panel.legend.min" class="cr1"></label>
|
||||
</li>
|
||||
<li class="tight-form-item">
|
||||
Max
|
||||
<input class="cr1" id="panel.legend.max" type="checkbox"
|
||||
ng-model="panel.legend.max" ng-checked="panel.legend.max" ng-change="legendValuesOptionChanged()">
|
||||
<label for="panel.legend.max" class="cr1"></label>
|
||||
</li>
|
||||
<li class="tight-form-item">
|
||||
Avg
|
||||
<input class="cr1" id="panel.legend.avg" type="checkbox"
|
||||
ng-model="panel.legend.avg" ng-checked="panel.legend.avg" ng-change="legendValuesOptionChanged()">
|
||||
<label for="panel.legend.avg" class="cr1"></label>
|
||||
</li>
|
||||
<li class="tight-form-item">
|
||||
Current
|
||||
<input class="cr1" id="panel.legend.current" type="checkbox"
|
||||
ng-model="panel.legend.current" ng-checked="panel.legend.current" ng-change="legendValuesOptionChanged()">
|
||||
<label for="panel.legend.current" class="cr1"></label>
|
||||
</li>
|
||||
<li class="tight-form-item last">
|
||||
Total
|
||||
<input class="cr1" id="panel.legend.total" type="checkbox"
|
||||
ng-model="panel.legend.total" ng-checked="panel.legend.total" ng-change="legendValuesOptionChanged()">
|
||||
<label for="panel.legend.total" class="cr1"></label>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
<div class="tight-form last">
|
||||
<ul class="tight-form-list">
|
||||
<li class="tight-form-item" style="width: 100px">
|
||||
<strong>Decimals</strong>
|
||||
</li>
|
||||
<li>
|
||||
<input type="number" class="input-small tight-form-input" placeholder="auto" bs-tooltip="'Override automatic decimal precision for legend and tooltips'" data-placement="right"
|
||||
ng-model="panel.decimals" ng-change="render()" ng-model-onblur>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
526
public/app/panels/graph/graph.js
Executable file
526
public/app/panels/graph/graph.js
Executable file
@@ -0,0 +1,526 @@
|
||||
define([
|
||||
'angular',
|
||||
'jquery',
|
||||
'kbn',
|
||||
'moment',
|
||||
'lodash',
|
||||
'./graph.tooltip',
|
||||
'jquery.flot',
|
||||
'jquery.flot.events',
|
||||
'jquery.flot.selection',
|
||||
'jquery.flot.time',
|
||||
'jquery.flot.stack',
|
||||
'jquery.flot.stackpercent',
|
||||
'jquery.flot.fillbelow',
|
||||
'jquery.flot.crosshair'
|
||||
],
|
||||
function (angular, $, kbn, moment, _, GraphTooltip) {
|
||||
'use strict';
|
||||
|
||||
var module = angular.module('grafana.directives');
|
||||
|
||||
module.directive('grafanaGraph', function($rootScope, timeSrv) {
|
||||
return {
|
||||
restrict: 'A',
|
||||
template: '<div> </div>',
|
||||
link: function(scope, elem) {
|
||||
var dashboard = scope.dashboard;
|
||||
var data, annotations;
|
||||
var sortedSeries;
|
||||
var graphHeight;
|
||||
var legendSideLastValue = null;
|
||||
scope.crosshairEmiter = false;
|
||||
|
||||
scope.onAppEvent('setCrosshair', function(event, info) {
|
||||
// do not need to to this if event is from this panel
|
||||
if (info.scope === scope) {
|
||||
return;
|
||||
}
|
||||
|
||||
if(dashboard.sharedCrosshair) {
|
||||
var plot = elem.data().plot;
|
||||
if (plot) {
|
||||
plot.setCrosshair({ x: info.pos.x, y: info.pos.y });
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
scope.onAppEvent('clearCrosshair', function() {
|
||||
var plot = elem.data().plot;
|
||||
if (plot) {
|
||||
plot.clearCrosshair();
|
||||
}
|
||||
});
|
||||
|
||||
// Receive render events
|
||||
scope.$on('render',function(event, renderData) {
|
||||
data = renderData || data;
|
||||
if (!data) {
|
||||
scope.get_data();
|
||||
return;
|
||||
}
|
||||
annotations = data.annotations || annotations;
|
||||
render_panel();
|
||||
});
|
||||
|
||||
function setElementHeight() {
|
||||
try {
|
||||
graphHeight = scope.height || scope.panel.height || scope.row.height;
|
||||
if (_.isString(graphHeight)) {
|
||||
graphHeight = parseInt(graphHeight.replace('px', ''), 10);
|
||||
}
|
||||
|
||||
graphHeight -= 5; // padding
|
||||
graphHeight -= scope.panel.title ? 24 : 9; // subtract panel title bar
|
||||
|
||||
if (scope.panel.legend.show && !scope.panel.legend.rightSide) {
|
||||
graphHeight = graphHeight - 26; // subtract one line legend
|
||||
}
|
||||
|
||||
elem.css('height', graphHeight + 'px');
|
||||
|
||||
return true;
|
||||
} catch(e) { // IE throws errors sometimes
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function shouldAbortRender() {
|
||||
if (!data) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if ($rootScope.fullscreen && !scope.fullscreen) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!setElementHeight()) { return true; }
|
||||
|
||||
if (_.isString(data)) {
|
||||
render_panel_as_graphite_png(data);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (elem.width() === 0) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
function drawHook(plot) {
|
||||
// Update legend values
|
||||
var yaxis = plot.getYAxes();
|
||||
for (var i = 0; i < data.length; i++) {
|
||||
var series = data[i];
|
||||
var axis = yaxis[series.yaxis - 1];
|
||||
var formater = kbn.valueFormats[scope.panel.y_formats[series.yaxis - 1]];
|
||||
|
||||
// decimal override
|
||||
if (scope.panel.decimals) {
|
||||
series.updateLegendValues(formater, scope.panel.decimals, null);
|
||||
} else {
|
||||
// auto decimals
|
||||
// legend and tooltip gets one more decimal precision
|
||||
// than graph legend ticks
|
||||
var tickDecimals = (axis.tickDecimals || -1) + 1;
|
||||
series.updateLegendValues(formater, tickDecimals, axis.scaledDecimals + 2);
|
||||
}
|
||||
|
||||
if(!scope.$$phase) { scope.$digest(); }
|
||||
}
|
||||
|
||||
// add left axis labels
|
||||
if (scope.panel.leftYAxisLabel) {
|
||||
var yaxisLabel = $("<div class='axisLabel left-yaxis-label'></div>")
|
||||
.text(scope.panel.leftYAxisLabel)
|
||||
.appendTo(elem);
|
||||
|
||||
yaxisLabel.css("margin-top", yaxisLabel.width() / 2);
|
||||
}
|
||||
|
||||
// add right axis labels
|
||||
if (scope.panel.rightYAxisLabel) {
|
||||
var rightLabel = $("<div class='axisLabel right-yaxis-label'></div>")
|
||||
.text(scope.panel.rightYAxisLabel)
|
||||
.appendTo(elem);
|
||||
|
||||
rightLabel.css("margin-top", rightLabel.width() / 2);
|
||||
}
|
||||
}
|
||||
|
||||
function processOffsetHook(plot, gridMargin) {
|
||||
if (scope.panel.leftYAxisLabel) { gridMargin.left = 20; }
|
||||
if (scope.panel.rightYAxisLabel) { gridMargin.right = 20; }
|
||||
}
|
||||
|
||||
// Function for rendering panel
|
||||
function render_panel() {
|
||||
if (shouldAbortRender()) {
|
||||
return;
|
||||
}
|
||||
|
||||
var panel = scope.panel;
|
||||
var stack = panel.stack ? true : null;
|
||||
|
||||
// Populate element
|
||||
var options = {
|
||||
hooks: {
|
||||
draw: [drawHook],
|
||||
processOffset: [processOffsetHook],
|
||||
},
|
||||
legend: { show: false },
|
||||
series: {
|
||||
stackpercent: panel.stack ? panel.percentage : false,
|
||||
stack: panel.percentage ? null : stack,
|
||||
lines: {
|
||||
show: panel.lines,
|
||||
zero: false,
|
||||
fill: translateFillOption(panel.fill),
|
||||
lineWidth: panel.linewidth,
|
||||
steps: panel.steppedLine
|
||||
},
|
||||
bars: {
|
||||
show: panel.bars,
|
||||
fill: 1,
|
||||
barWidth: 1,
|
||||
zero: false,
|
||||
lineWidth: 0
|
||||
},
|
||||
points: {
|
||||
show: panel.points,
|
||||
fill: 1,
|
||||
fillColor: false,
|
||||
radius: panel.points ? panel.pointradius : 2
|
||||
// little points when highlight points
|
||||
},
|
||||
shadowSize: 1
|
||||
},
|
||||
yaxes: [],
|
||||
xaxis: {},
|
||||
grid: {
|
||||
minBorderMargin: 0,
|
||||
markings: [],
|
||||
backgroundColor: null,
|
||||
borderWidth: 0,
|
||||
hoverable: true,
|
||||
color: '#c8c8c8',
|
||||
margin: { left: 0, right: 0 },
|
||||
},
|
||||
selection: {
|
||||
mode: "x",
|
||||
color: '#666'
|
||||
},
|
||||
crosshair: {
|
||||
mode: panel.tooltip.shared || dashboard.sharedCrosshair ? "x" : null
|
||||
}
|
||||
};
|
||||
|
||||
for (var i = 0; i < data.length; i++) {
|
||||
var series = data[i];
|
||||
series.applySeriesOverrides(panel.seriesOverrides);
|
||||
series.data = series.getFlotPairs(panel.nullPointMode, panel.y_formats);
|
||||
|
||||
// if hidden remove points and disable stack
|
||||
if (scope.hiddenSeries[series.alias]) {
|
||||
series.data = [];
|
||||
series.stack = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (data.length && data[0].stats.timeStep) {
|
||||
options.series.bars.barWidth = data[0].stats.timeStep / 1.5;
|
||||
}
|
||||
|
||||
addTimeAxis(options);
|
||||
addGridThresholds(options, panel);
|
||||
addAnnotations(options);
|
||||
configureAxisOptions(data, options);
|
||||
|
||||
sortedSeries = _.sortBy(data, function(series) { return series.zindex; });
|
||||
|
||||
function callPlot() {
|
||||
try {
|
||||
$.plot(elem, sortedSeries, options);
|
||||
} catch (e) {
|
||||
console.log('flotcharts error', e);
|
||||
}
|
||||
}
|
||||
|
||||
if (shouldDelayDraw(panel)) {
|
||||
// temp fix for legends on the side, need to render twice to get dimensions right
|
||||
callPlot();
|
||||
setTimeout(callPlot, 50);
|
||||
legendSideLastValue = panel.legend.rightSide;
|
||||
}
|
||||
else {
|
||||
callPlot();
|
||||
}
|
||||
}
|
||||
|
||||
function translateFillOption(fill) {
|
||||
return fill === 0 ? 0.001 : fill/10;
|
||||
}
|
||||
|
||||
function shouldDelayDraw(panel) {
|
||||
if (panel.legend.rightSide) {
|
||||
return true;
|
||||
}
|
||||
if (legendSideLastValue !== null && panel.legend.rightSide !== legendSideLastValue) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function addTimeAxis(options) {
|
||||
var ticks = elem.width() / 100;
|
||||
var min = _.isUndefined(scope.range.from) ? null : scope.range.from.getTime();
|
||||
var max = _.isUndefined(scope.range.to) ? null : scope.range.to.getTime();
|
||||
|
||||
options.xaxis = {
|
||||
timezone: dashboard.timezone,
|
||||
show: scope.panel['x-axis'],
|
||||
mode: "time",
|
||||
min: min,
|
||||
max: max,
|
||||
label: "Datetime",
|
||||
ticks: ticks,
|
||||
timeformat: time_format(scope.interval, ticks, min, max),
|
||||
};
|
||||
}
|
||||
|
||||
function addGridThresholds(options, panel) {
|
||||
if (_.isNumber(panel.grid.threshold1)) {
|
||||
var limit1 = panel.grid.thresholdLine ? panel.grid.threshold1 : (panel.grid.threshold2 || null);
|
||||
options.grid.markings.push({
|
||||
yaxis: { from: panel.grid.threshold1, to: limit1 },
|
||||
color: panel.grid.threshold1Color
|
||||
});
|
||||
|
||||
if (_.isNumber(panel.grid.threshold2)) {
|
||||
var limit2;
|
||||
if (panel.grid.thresholdLine) {
|
||||
limit2 = panel.grid.threshold2;
|
||||
} else {
|
||||
limit2 = panel.grid.threshold1 > panel.grid.threshold2 ? -Infinity : +Infinity;
|
||||
}
|
||||
options.grid.markings.push({
|
||||
yaxis: { from: panel.grid.threshold2, to: limit2 },
|
||||
color: panel.grid.threshold2Color
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function addAnnotations(options) {
|
||||
if(!annotations || annotations.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
var types = {};
|
||||
|
||||
_.each(annotations, function(event) {
|
||||
if (!types[event.annotation.name]) {
|
||||
types[event.annotation.name] = {
|
||||
level: _.keys(types).length + 1,
|
||||
icon: {
|
||||
icon: "fa fa-chevron-down",
|
||||
size: event.annotation.iconSize,
|
||||
color: event.annotation.iconColor,
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
if (event.annotation.showLine) {
|
||||
options.grid.markings.push({
|
||||
color: event.annotation.lineColor,
|
||||
lineWidth: 1,
|
||||
xaxis: { from: event.min, to: event.max }
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
options.events = {
|
||||
levels: _.keys(types).length + 1,
|
||||
data: annotations,
|
||||
types: types
|
||||
};
|
||||
}
|
||||
|
||||
function configureAxisOptions(data, options) {
|
||||
var defaults = {
|
||||
position: 'left',
|
||||
show: scope.panel['y-axis'],
|
||||
min: scope.panel.grid.leftMin,
|
||||
index: 1,
|
||||
logBase: scope.panel.grid.leftLogBase || 1,
|
||||
max: scope.panel.percentage && scope.panel.stack ? 100 : scope.panel.grid.leftMax,
|
||||
};
|
||||
|
||||
options.yaxes.push(defaults);
|
||||
|
||||
if (_.findWhere(data, {yaxis: 2})) {
|
||||
var secondY = _.clone(defaults);
|
||||
secondY.index = 2,
|
||||
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;
|
||||
options.yaxes.push(secondY);
|
||||
|
||||
applyLogScale(options.yaxes[1], data);
|
||||
configureAxisMode(options.yaxes[1], scope.panel.y_formats[1]);
|
||||
}
|
||||
|
||||
applyLogScale(options.yaxes[0], data);
|
||||
configureAxisMode(options.yaxes[0], scope.panel.y_formats[0]);
|
||||
}
|
||||
|
||||
function applyLogScale(axis, data) {
|
||||
if (axis.logBase === 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
var series, i;
|
||||
var max = axis.max;
|
||||
|
||||
if (max === null) {
|
||||
for (i = 0; i < data.length; i++) {
|
||||
series = data[i];
|
||||
if (series.yaxis === axis.index) {
|
||||
if (max < series.stats.max) {
|
||||
max = series.stats.max;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (max === void 0) {
|
||||
max = Number.MAX_VALUE;
|
||||
}
|
||||
}
|
||||
|
||||
axis.min = axis.min !== null ? axis.min : 0;
|
||||
axis.ticks = [0, 1];
|
||||
var nextTick = 1;
|
||||
|
||||
while (true) {
|
||||
nextTick = nextTick * axis.logBase;
|
||||
axis.ticks.push(nextTick);
|
||||
if (nextTick > max) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
axis.tickFormatter = function(val, axis) {
|
||||
return kbn.valueFormats[format](val, axis.tickDecimals, axis.scaledDecimals);
|
||||
};
|
||||
}
|
||||
|
||||
function time_format(interval, ticks, min, max) {
|
||||
if (min && max && ticks) {
|
||||
var secPerTick = ((max - min) / ticks) / 1000;
|
||||
|
||||
if (secPerTick <= 45) {
|
||||
return "%H:%M:%S";
|
||||
}
|
||||
if (secPerTick <= 7200) {
|
||||
return "%H:%M";
|
||||
}
|
||||
if (secPerTick <= 80000) {
|
||||
return "%m/%d %H:%M";
|
||||
}
|
||||
if (secPerTick <= 2419200) {
|
||||
return "%m/%d";
|
||||
}
|
||||
return "%Y-%m";
|
||||
}
|
||||
|
||||
return "%H:%M";
|
||||
}
|
||||
|
||||
function render_panel_as_graphite_png(url) {
|
||||
url += '&width=' + elem.width();
|
||||
url += '&height=' + elem.css('height').replace('px', '');
|
||||
url += '&bgcolor=1f1f1f'; // @grayDarker & @grafanaPanelBackground
|
||||
url += '&fgcolor=BBBFC2'; // @textColor & @grayLighter
|
||||
url += scope.panel.stack ? '&areaMode=stacked' : '';
|
||||
url += scope.panel.fill !== 0 ? ('&areaAlpha=' + (scope.panel.fill/10).toFixed(1)) : '';
|
||||
url += scope.panel.linewidth !== 0 ? '&lineWidth=' + scope.panel.linewidth : '';
|
||||
url += scope.panel.legend.show ? '&hideLegend=false' : '&hideLegend=true';
|
||||
url += scope.panel.grid.leftMin !== null ? '&yMin=' + scope.panel.grid.leftMin : '';
|
||||
url += scope.panel.grid.leftMax !== null ? '&yMax=' + scope.panel.grid.leftMax : '';
|
||||
url += scope.panel.grid.rightMin !== null ? '&yMin=' + scope.panel.grid.rightMin : '';
|
||||
url += scope.panel.grid.rightMax !== null ? '&yMax=' + scope.panel.grid.rightMax : '';
|
||||
url += scope.panel['x-axis'] ? '' : '&hideAxes=true';
|
||||
url += scope.panel['y-axis'] ? '' : '&hideYAxis=true';
|
||||
|
||||
switch(scope.panel.y_formats[0]) {
|
||||
case 'bytes':
|
||||
url += '&yUnitSystem=binary';
|
||||
break;
|
||||
case 'bits':
|
||||
url += '&yUnitSystem=binary';
|
||||
break;
|
||||
case 'bps':
|
||||
url += '&yUnitSystem=si';
|
||||
break;
|
||||
case 'Bps':
|
||||
url += '&yUnitSystem=si';
|
||||
break;
|
||||
case 'short':
|
||||
url += '&yUnitSystem=si';
|
||||
break;
|
||||
case 'joule':
|
||||
url += '&yUnitSystem=si';
|
||||
break;
|
||||
case 'watt':
|
||||
url += '&yUnitSystem=si';
|
||||
break;
|
||||
case 'ev':
|
||||
url += '&yUnitSystem=si';
|
||||
break;
|
||||
case 'none':
|
||||
url += '&yUnitSystem=none';
|
||||
break;
|
||||
}
|
||||
|
||||
switch(scope.panel.nullPointMode) {
|
||||
case 'connected':
|
||||
url += '&lineMode=connected';
|
||||
break;
|
||||
case 'null':
|
||||
break; // graphite default lineMode
|
||||
case 'null as zero':
|
||||
url += "&drawNullAsZero=true";
|
||||
break;
|
||||
}
|
||||
|
||||
url += scope.panel.steppedLine ? '&lineMode=staircase' : '';
|
||||
|
||||
elem.html('<img src="' + url + '"></img>');
|
||||
}
|
||||
|
||||
new GraphTooltip(elem, dashboard, scope, function() {
|
||||
return sortedSeries;
|
||||
});
|
||||
|
||||
elem.bind("plotselected", function (event, ranges) {
|
||||
scope.$apply(function() {
|
||||
timeSrv.setTime({
|
||||
from : moment.utc(ranges.xaxis.from).toDate(),
|
||||
to : moment.utc(ranges.xaxis.to).toDate(),
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
});
|
||||
162
public/app/panels/graph/graph.tooltip.js
Normal file
162
public/app/panels/graph/graph.tooltip.js
Normal file
@@ -0,0 +1,162 @@
|
||||
define([
|
||||
'jquery',
|
||||
],
|
||||
function ($) {
|
||||
'use strict';
|
||||
|
||||
function GraphTooltip(elem, dashboard, scope, getSeriesFn) {
|
||||
var self = this;
|
||||
|
||||
var $tooltip = $('<div id="tooltip">');
|
||||
|
||||
this.findHoverIndexFromDataPoints = function(posX, series, last) {
|
||||
var ps = series.datapoints.pointsize;
|
||||
var initial = last*ps;
|
||||
var len = series.datapoints.points.length;
|
||||
for (var j = initial; j < len; j += ps) {
|
||||
if (series.datapoints.points[j] > posX) {
|
||||
return Math.max(j - ps, 0)/ps;
|
||||
}
|
||||
}
|
||||
return j/ps - 1;
|
||||
};
|
||||
|
||||
this.findHoverIndexFromData = function(posX, series) {
|
||||
var len = series.data.length;
|
||||
for (var j = 0; j < len; j++) {
|
||||
if (series.data[j][0] > posX) {
|
||||
return Math.max(j - 1, 0);
|
||||
}
|
||||
}
|
||||
return j - 1;
|
||||
};
|
||||
|
||||
this.showTooltip = function(title, innerHtml, pos) {
|
||||
var body = '<div class="graph-tooltip small"><div class="graph-tooltip-time">'+ title + '</div> ' ;
|
||||
body += innerHtml + '</div>';
|
||||
$tooltip.html(body).place_tt(pos.pageX + 20, pos.pageY);
|
||||
};
|
||||
|
||||
this.getMultiSeriesPlotHoverInfo = function(seriesList, pos) {
|
||||
var value, i, series, hoverIndex;
|
||||
var results = [];
|
||||
|
||||
//now we know the current X (j) position for X and Y values
|
||||
var last_value = 0; //needed for stacked values
|
||||
|
||||
for (i = 0; i < seriesList.length; i++) {
|
||||
series = seriesList[i];
|
||||
|
||||
if (!series.data.length || (scope.panel.legend.hideEmpty && series.allIsNull)) {
|
||||
results.push({ hidden: true });
|
||||
continue;
|
||||
}
|
||||
|
||||
hoverIndex = this.findHoverIndexFromData(pos.x, series);
|
||||
results.time = series.data[hoverIndex][0];
|
||||
|
||||
if (scope.panel.stack) {
|
||||
if (scope.panel.tooltip.value_type === 'individual') {
|
||||
value = series.data[hoverIndex][1];
|
||||
} else {
|
||||
last_value += series.data[hoverIndex][1];
|
||||
value = last_value;
|
||||
}
|
||||
} else {
|
||||
value = series.data[hoverIndex][1];
|
||||
}
|
||||
|
||||
// Highlighting multiple Points depending on the plot type
|
||||
if (scope.panel.steppedLine || (scope.panel.stack && scope.panel.nullPointMode == "null")) {
|
||||
// stacked and steppedLine plots can have series with different length.
|
||||
// Stacked series can increase its length on each new stacked serie if null points found,
|
||||
// to speed the index search we begin always on the last found hoverIndex.
|
||||
var newhoverIndex = this.findHoverIndexFromDataPoints(pos.x, series, hoverIndex);
|
||||
results.push({ value: value, hoverIndex: newhoverIndex});
|
||||
} else {
|
||||
results.push({ value: value, hoverIndex: hoverIndex});
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
};
|
||||
|
||||
elem.mouseleave(function () {
|
||||
if (scope.panel.tooltip.shared || dashboard.sharedCrosshair) {
|
||||
var plot = elem.data().plot;
|
||||
if (plot) {
|
||||
$tooltip.detach();
|
||||
plot.unhighlight();
|
||||
scope.appEvent('clearCrosshair');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
elem.bind("plothover", function (event, pos, item) {
|
||||
var plot = elem.data().plot;
|
||||
var plotData = plot.getData();
|
||||
var seriesList = getSeriesFn();
|
||||
var group, value, timestamp, hoverInfo, i, series, seriesHtml;
|
||||
|
||||
if(dashboard.sharedCrosshair){
|
||||
scope.appEvent('setCrosshair', { pos: pos, scope: scope });
|
||||
}
|
||||
|
||||
if (seriesList.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (scope.panel.tooltip.shared) {
|
||||
plot.unhighlight();
|
||||
|
||||
var seriesHoverInfo = self.getMultiSeriesPlotHoverInfo(plotData, pos);
|
||||
|
||||
seriesHtml = '';
|
||||
timestamp = dashboard.formatDate(seriesHoverInfo.time);
|
||||
|
||||
for (i = 0; i < seriesHoverInfo.length; i++) {
|
||||
hoverInfo = seriesHoverInfo[i];
|
||||
|
||||
if (hoverInfo.hidden) {
|
||||
continue;
|
||||
}
|
||||
|
||||
series = seriesList[i];
|
||||
value = series.formatValue(hoverInfo.value);
|
||||
|
||||
seriesHtml += '<div class="graph-tooltip-list-item"><div class="graph-tooltip-series-name">';
|
||||
seriesHtml += '<i class="fa fa-minus" style="color:' + series.color +';"></i> ' + series.label + ':</div>';
|
||||
seriesHtml += '<div class="graph-tooltip-value">' + value + '</div></div>';
|
||||
plot.highlight(i, hoverInfo.hoverIndex);
|
||||
}
|
||||
|
||||
self.showTooltip(timestamp, seriesHtml, pos);
|
||||
}
|
||||
// single series tooltip
|
||||
else if (item) {
|
||||
series = seriesList[item.seriesIndex];
|
||||
group = '<div class="graph-tooltip-list-item"><div class="graph-tooltip-series-name">';
|
||||
group += '<i class="fa fa-minus" style="color:' + item.series.color +';"></i> ' + series.label + ':</div>';
|
||||
|
||||
if (scope.panel.stack && scope.panel.tooltip.value_type === 'individual') {
|
||||
value = item.datapoint[1] - item.datapoint[2];
|
||||
}
|
||||
else {
|
||||
value = item.datapoint[1];
|
||||
}
|
||||
|
||||
value = series.formatValue(value);
|
||||
timestamp = dashboard.formatDate(item.datapoint[0]);
|
||||
group += '<div class="graph-tooltip-value">' + value + '</div>';
|
||||
|
||||
self.showTooltip(timestamp, group, pos);
|
||||
}
|
||||
// no hit
|
||||
else {
|
||||
$tooltip.detach();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return GraphTooltip;
|
||||
});
|
||||
168
public/app/panels/graph/legend.js
Normal file
168
public/app/panels/graph/legend.js
Normal file
@@ -0,0 +1,168 @@
|
||||
define([
|
||||
'angular',
|
||||
'app',
|
||||
'lodash',
|
||||
'kbn',
|
||||
'jquery',
|
||||
'jquery.flot',
|
||||
'jquery.flot.time',
|
||||
],
|
||||
function (angular, app, _, kbn, $) {
|
||||
'use strict';
|
||||
|
||||
var module = angular.module('grafana.panels.graph');
|
||||
|
||||
module.directive('graphLegend', function(popoverSrv) {
|
||||
|
||||
return {
|
||||
link: function(scope, elem) {
|
||||
var $container = $('<section class="graph-legend"></section>');
|
||||
var firstRender = true;
|
||||
var panel = scope.panel;
|
||||
var data;
|
||||
var seriesList;
|
||||
var i;
|
||||
|
||||
scope.$on('render', function() {
|
||||
data = scope.seriesList;
|
||||
if (data) {
|
||||
render();
|
||||
}
|
||||
});
|
||||
|
||||
function getSeriesIndexForElement(el) {
|
||||
return el.parents('[data-series-index]').data('series-index');
|
||||
}
|
||||
|
||||
function openColorSelector(e) {
|
||||
var el = $(e.currentTarget);
|
||||
var index = getSeriesIndexForElement(el);
|
||||
var seriesInfo = seriesList[index];
|
||||
var popoverScope = scope.$new();
|
||||
popoverScope.series = seriesInfo;
|
||||
popoverSrv.show({
|
||||
element: $(':first-child', el),
|
||||
templateUrl: 'app/panels/graph/legend.popover.html',
|
||||
scope: popoverScope
|
||||
});
|
||||
}
|
||||
|
||||
function toggleSeries(e) {
|
||||
var el = $(e.currentTarget);
|
||||
var index = getSeriesIndexForElement(el);
|
||||
var seriesInfo = seriesList[index];
|
||||
scope.toggleSeries(seriesInfo, e);
|
||||
}
|
||||
|
||||
function sortLegend(e) {
|
||||
var el = $(e.currentTarget);
|
||||
var stat = el.data('stat');
|
||||
|
||||
if (stat !== panel.legend.sort) { panel.legend.sortDesc = null; }
|
||||
|
||||
// if already sort ascending, disable sorting
|
||||
if (panel.legend.sortDesc === false) {
|
||||
panel.legend.sort = null;
|
||||
panel.legend.sortDesc = null;
|
||||
render();
|
||||
return;
|
||||
}
|
||||
|
||||
panel.legend.sortDesc = !panel.legend.sortDesc;
|
||||
panel.legend.sort = stat;
|
||||
render();
|
||||
}
|
||||
|
||||
function getTableHeaderHtml(statName) {
|
||||
if (!panel.legend[statName]) { return ""; }
|
||||
var html = '<th class="pointer" data-stat="' + statName + '">' + statName;
|
||||
|
||||
if (panel.legend.sort === statName) {
|
||||
var cssClass = panel.legend.sortDesc ? 'fa fa-caret-down' : 'fa fa-caret-up' ;
|
||||
html += ' <span class="' + cssClass + '"></span>';
|
||||
}
|
||||
|
||||
return html + '</th>';
|
||||
}
|
||||
|
||||
function render() {
|
||||
if (firstRender) {
|
||||
elem.append($container);
|
||||
$container.on('click', '.graph-legend-icon', openColorSelector);
|
||||
$container.on('click', '.graph-legend-alias', toggleSeries);
|
||||
$container.on('click', 'th', sortLegend);
|
||||
firstRender = false;
|
||||
}
|
||||
|
||||
seriesList = data;
|
||||
|
||||
$container.empty();
|
||||
|
||||
$container.toggleClass('graph-legend-table', panel.legend.alignAsTable === true);
|
||||
|
||||
if (panel.legend.alignAsTable) {
|
||||
var header = '<tr>';
|
||||
header += '<th colspan="2" style="text-align:left"></th>';
|
||||
if (panel.legend.values) {
|
||||
header += getTableHeaderHtml('min');
|
||||
header += getTableHeaderHtml('max');
|
||||
header += getTableHeaderHtml('avg');
|
||||
header += getTableHeaderHtml('current');
|
||||
header += getTableHeaderHtml('total');
|
||||
}
|
||||
header += '</tr>';
|
||||
$container.append($(header));
|
||||
}
|
||||
|
||||
if (panel.legend.sort) {
|
||||
seriesList = _.sortBy(seriesList, function(series) {
|
||||
return series.stats[panel.legend.sort];
|
||||
});
|
||||
if (panel.legend.sortDesc) {
|
||||
seriesList = seriesList.reverse();
|
||||
}
|
||||
}
|
||||
|
||||
for (i = 0; i < seriesList.length; i++) {
|
||||
var series = seriesList[i];
|
||||
|
||||
// ignore empty series
|
||||
if (panel.legend.hideEmpty && series.allIsNull) {
|
||||
continue;
|
||||
}
|
||||
|
||||
var html = '<div class="graph-legend-series';
|
||||
if (series.yaxis === 2) { html += ' pull-right'; }
|
||||
if (scope.hiddenSeries[series.alias]) { html += ' graph-legend-series-hidden'; }
|
||||
html += '" data-series-index="' + i + '">';
|
||||
html += '<div class="graph-legend-icon">';
|
||||
html += '<i class="fa fa-minus pointer" style="color:' + series.color + '"></i>';
|
||||
html += '</div>';
|
||||
|
||||
html += '<div class="graph-legend-alias">';
|
||||
html += '<a>' + series.label + '</a>';
|
||||
html += '</div>';
|
||||
|
||||
if (panel.legend.values) {
|
||||
var avg = series.formatValue(series.stats.avg);
|
||||
var current = series.formatValue(series.stats.current);
|
||||
var min = series.formatValue(series.stats.min);
|
||||
var max = series.formatValue(series.stats.max);
|
||||
var total = series.formatValue(series.stats.total);
|
||||
|
||||
if (panel.legend.min) { html += '<div class="graph-legend-value min">' + min + '</div>'; }
|
||||
if (panel.legend.max) { html += '<div class="graph-legend-value max">' + max + '</div>'; }
|
||||
if (panel.legend.avg) { html += '<div class="graph-legend-value avg">' + avg + '</div>'; }
|
||||
if (panel.legend.current) { html += '<div class="graph-legend-value current">' + current + '</div>'; }
|
||||
if (panel.legend.total) { html += '<div class="graph-legend-value total">' + total + '</div>'; }
|
||||
}
|
||||
|
||||
html += '</div>';
|
||||
$container.append($(html));
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
});
|
||||
26
public/app/panels/graph/legend.popover.html
Normal file
26
public/app/panels/graph/legend.popover.html
Normal file
@@ -0,0 +1,26 @@
|
||||
<div class="graph-legend-popover">
|
||||
<a class="close" ng-click="dismiss();" href="">×</a>
|
||||
|
||||
<div class="editor-row small" style="padding-bottom: 0;">
|
||||
<label>Axis:</label>
|
||||
<button ng-click="toggleYAxis(series);dismiss();"
|
||||
class="btn btn-mini"
|
||||
ng-class="{'btn-success': series.yaxis === 1 }">
|
||||
Left
|
||||
</button>
|
||||
<button ng-click="toggleYAxis(series);dismiss();"
|
||||
class="btn btn-mini"
|
||||
ng-class="{'btn-success': series.yaxis === 2 }">
|
||||
Right
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="editor-row">
|
||||
<i ng-repeat="color in colors"
|
||||
class="pointer"
|
||||
ng-class="{'fa fa-circle-o': color === series.color,'fa fa-circle': color !== series.color}"
|
||||
ng-style="{color:color}"
|
||||
ng-click="changeSeriesColor(series, color);dismiss();"> </i>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
29
public/app/panels/graph/module.html
Normal file
29
public/app/panels/graph/module.html
Normal file
@@ -0,0 +1,29 @@
|
||||
<grafana-panel>
|
||||
|
||||
<div class="graph-wrapper" ng-class="{'graph-legend-rightside': panel.legend.rightSide}">
|
||||
<div class="graph-canvas-wrapper">
|
||||
|
||||
<!-- <span class="graph-time-info" ng-if="panelMeta.timeInfo"> -->
|
||||
<!-- <i class="fa fa-clock-o"></i> {{panelMeta.timeInfo}} -->
|
||||
<!-- </span> -->
|
||||
|
||||
<div ng-if="datapointsWarning" class="datapoints-warning">
|
||||
<span class="small" ng-show="!datapointsCount">
|
||||
No datapoints <tip>No datapoints returned from metric query</tip>
|
||||
</span>
|
||||
<span class="small" ng-show="datapointsOutside">Datapoints outside time range <tip>Can be caused by timezone mismatch between browser and graphite server</tip></span>
|
||||
</div>
|
||||
|
||||
<div grafana-graph class="histogram-chart">
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="graph-legend-wrapper" ng-if="panel.legend.show" graph-legend></div>
|
||||
</div>
|
||||
|
||||
<div class="clearfix"></div>
|
||||
|
||||
</grafana-panel>
|
||||
|
||||
|
||||
297
public/app/panels/graph/module.js
Normal file
297
public/app/panels/graph/module.js
Normal file
@@ -0,0 +1,297 @@
|
||||
define([
|
||||
'angular',
|
||||
'app',
|
||||
'jquery',
|
||||
'lodash',
|
||||
'kbn',
|
||||
'moment',
|
||||
'components/timeSeries',
|
||||
'components/panelmeta',
|
||||
'./seriesOverridesCtrl',
|
||||
'./graph',
|
||||
'./legend',
|
||||
],
|
||||
function (angular, app, $, _, kbn, moment, TimeSeries, PanelMeta) {
|
||||
'use strict';
|
||||
|
||||
var module = angular.module('grafana.panels.graph');
|
||||
|
||||
module.directive('grafanaPanelGraph', function() {
|
||||
return {
|
||||
controller: 'GraphCtrl',
|
||||
templateUrl: 'app/panels/graph/module.html',
|
||||
};
|
||||
});
|
||||
|
||||
module.controller('GraphCtrl', function($scope, $rootScope, panelSrv, annotationsSrv, panelHelper, $q) {
|
||||
|
||||
$scope.panelMeta = new PanelMeta({
|
||||
panelName: 'Graph',
|
||||
editIcon: "fa fa-bar-chart",
|
||||
fullscreen: true,
|
||||
metricsEditor: true
|
||||
});
|
||||
|
||||
$scope.panelMeta.addEditorTab('Axes & Grid', 'app/panels/graph/axisEditor.html');
|
||||
$scope.panelMeta.addEditorTab('Display Styles', 'app/panels/graph/styleEditor.html');
|
||||
$scope.panelMeta.addEditorTab('Time range', 'app/features/panel/partials/panelTime.html');
|
||||
|
||||
$scope.panelMeta.addExtendedMenuItem('Export CSV', '', 'exportCsv()');
|
||||
$scope.panelMeta.addExtendedMenuItem('Toggle legend', '', 'toggleLegend()');
|
||||
|
||||
// Set and populate defaults
|
||||
var _d = {
|
||||
// datasource name, null = default datasource
|
||||
datasource: null,
|
||||
// sets client side (flot) or native graphite png renderer (png)
|
||||
renderer: 'flot',
|
||||
// Show/hide the x-axis
|
||||
'x-axis' : true,
|
||||
// Show/hide y-axis
|
||||
'y-axis' : true,
|
||||
// y axis formats, [left axis,right axis]
|
||||
y_formats : ['short', 'short'],
|
||||
// grid options
|
||||
grid : {
|
||||
leftLogBase: 1,
|
||||
leftMax: null,
|
||||
rightMax: null,
|
||||
leftMin: null,
|
||||
rightMin: null,
|
||||
rightLogBase: 1,
|
||||
threshold1: null,
|
||||
threshold2: null,
|
||||
threshold1Color: 'rgba(216, 200, 27, 0.27)',
|
||||
threshold2Color: 'rgba(234, 112, 112, 0.22)'
|
||||
},
|
||||
// show/hide lines
|
||||
lines : true,
|
||||
// fill factor
|
||||
fill : 0,
|
||||
// line width in pixels
|
||||
linewidth : 1,
|
||||
// show hide points
|
||||
points : false,
|
||||
// point radius in pixels
|
||||
pointradius : 5,
|
||||
// show hide bars
|
||||
bars : false,
|
||||
// enable/disable stacking
|
||||
stack : false,
|
||||
// stack percentage mode
|
||||
percentage : false,
|
||||
// legend options
|
||||
legend: {
|
||||
show: true, // disable/enable legend
|
||||
values: false, // disable/enable legend values
|
||||
min: false,
|
||||
max: false,
|
||||
current: false,
|
||||
total: false,
|
||||
avg: false
|
||||
},
|
||||
// how null points should be handled
|
||||
nullPointMode : 'connected',
|
||||
// staircase line mode
|
||||
steppedLine: false,
|
||||
// tooltip options
|
||||
tooltip : {
|
||||
value_type: 'cumulative',
|
||||
shared: true,
|
||||
},
|
||||
// time overrides
|
||||
timeFrom: null,
|
||||
timeShift: null,
|
||||
// metric queries
|
||||
targets: [{}],
|
||||
// series color overrides
|
||||
aliasColors: {},
|
||||
// other style overrides
|
||||
seriesOverrides: [],
|
||||
};
|
||||
|
||||
_.defaults($scope.panel,_d);
|
||||
_.defaults($scope.panel.tooltip, _d.tooltip);
|
||||
_.defaults($scope.panel.annotate, _d.annotate);
|
||||
_.defaults($scope.panel.grid, _d.grid);
|
||||
_.defaults($scope.panel.legend, _d.legend);
|
||||
|
||||
$scope.logScales = {'linear': 1, 'log (base 10)': 10, 'log (base 32)': 32, 'log (base 1024)': 1024};
|
||||
|
||||
$scope.hiddenSeries = {};
|
||||
$scope.seriesList = [];
|
||||
$scope.unitFormats = kbn.getUnitFormats();
|
||||
|
||||
$scope.setUnitFormat = function(axis, subItem) {
|
||||
$scope.panel.y_formats[axis] = subItem.value;
|
||||
$scope.render();
|
||||
};
|
||||
|
||||
$scope.refreshData = function(datasource) {
|
||||
panelHelper.updateTimeRange($scope);
|
||||
|
||||
$scope.annotationsPromise = annotationsSrv.getAnnotations($scope.rangeUnparsed, $scope.dashboard);
|
||||
|
||||
return panelHelper.issueMetricQuery($scope, datasource)
|
||||
.then($scope.dataHandler, function(err) {
|
||||
$scope.seriesList = [];
|
||||
$scope.render([]);
|
||||
throw err;
|
||||
});
|
||||
};
|
||||
|
||||
$scope.loadSnapshot = function(snapshotData) {
|
||||
panelHelper.updateTimeRange($scope);
|
||||
$scope.annotationsPromise = $q.when([]);
|
||||
$scope.dataHandler(snapshotData);
|
||||
};
|
||||
|
||||
$scope.dataHandler = function(results) {
|
||||
// png renderer returns just a url
|
||||
if (_.isString(results)) {
|
||||
$scope.render(results);
|
||||
return;
|
||||
}
|
||||
|
||||
$scope.datapointsWarning = false;
|
||||
$scope.datapointsCount = 0;
|
||||
$scope.datapointsOutside = false;
|
||||
|
||||
$scope.seriesList = _.map(results.data, $scope.seriesHandler);
|
||||
|
||||
$scope.datapointsWarning = $scope.datapointsCount === 0 || $scope.datapointsOutside;
|
||||
|
||||
$scope.annotationsPromise
|
||||
.then(function(annotations) {
|
||||
$scope.panelMeta.loading = false;
|
||||
$scope.seriesList.annotations = annotations;
|
||||
$scope.render($scope.seriesList);
|
||||
}, function() {
|
||||
$scope.panelMeta.loading = false;
|
||||
$scope.render($scope.seriesList);
|
||||
});
|
||||
};
|
||||
|
||||
$scope.seriesHandler = function(seriesData, index) {
|
||||
var datapoints = seriesData.datapoints;
|
||||
var alias = seriesData.target;
|
||||
var color = $scope.panel.aliasColors[alias] || $rootScope.colors[index];
|
||||
|
||||
var series = new TimeSeries({
|
||||
datapoints: datapoints,
|
||||
alias: alias,
|
||||
color: color,
|
||||
});
|
||||
|
||||
if (datapoints && datapoints.length > 0) {
|
||||
var last = moment.utc(datapoints[datapoints.length - 1][1]);
|
||||
var from = moment.utc($scope.range.from);
|
||||
if (last - from < -10000) {
|
||||
$scope.datapointsOutside = true;
|
||||
}
|
||||
|
||||
$scope.datapointsCount += datapoints.length;
|
||||
}
|
||||
|
||||
return series;
|
||||
};
|
||||
|
||||
$scope.render = function(data) {
|
||||
$scope.$broadcast('render', data);
|
||||
};
|
||||
|
||||
$scope.changeSeriesColor = function(series, color) {
|
||||
series.color = color;
|
||||
$scope.panel.aliasColors[series.alias] = series.color;
|
||||
$scope.render();
|
||||
};
|
||||
|
||||
$scope.toggleSeries = function(serie, event) {
|
||||
if (event.ctrlKey || event.metaKey || event.shiftKey) {
|
||||
if ($scope.hiddenSeries[serie.alias]) {
|
||||
delete $scope.hiddenSeries[serie.alias];
|
||||
}
|
||||
else {
|
||||
$scope.hiddenSeries[serie.alias] = true;
|
||||
}
|
||||
} else {
|
||||
$scope.toggleSeriesExclusiveMode(serie);
|
||||
}
|
||||
|
||||
$scope.render();
|
||||
};
|
||||
|
||||
$scope.toggleSeriesExclusiveMode = function(serie) {
|
||||
var hidden = $scope.hiddenSeries;
|
||||
|
||||
if (hidden[serie.alias]) {
|
||||
delete hidden[serie.alias];
|
||||
}
|
||||
|
||||
// check if every other series is hidden
|
||||
var alreadyExclusive = _.every($scope.seriesList, function(value) {
|
||||
if (value.alias === serie.alias) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return hidden[value.alias];
|
||||
});
|
||||
|
||||
if (alreadyExclusive) {
|
||||
// remove all hidden series
|
||||
_.each($scope.seriesList, function(value) {
|
||||
delete $scope.hiddenSeries[value.alias];
|
||||
});
|
||||
}
|
||||
else {
|
||||
// hide all but this serie
|
||||
_.each($scope.seriesList, function(value) {
|
||||
if (value.alias === serie.alias) {
|
||||
return;
|
||||
}
|
||||
|
||||
$scope.hiddenSeries[value.alias] = true;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
$scope.toggleYAxis = function(info) {
|
||||
var override = _.findWhere($scope.panel.seriesOverrides, { alias: info.alias });
|
||||
if (!override) {
|
||||
override = { alias: info.alias };
|
||||
$scope.panel.seriesOverrides.push(override);
|
||||
}
|
||||
override.yaxis = info.yaxis === 2 ? 1 : 2;
|
||||
$scope.render();
|
||||
};
|
||||
|
||||
$scope.addSeriesOverride = function(override) {
|
||||
$scope.panel.seriesOverrides.push(override || {});
|
||||
};
|
||||
|
||||
$scope.removeSeriesOverride = function(override) {
|
||||
$scope.panel.seriesOverrides = _.without($scope.panel.seriesOverrides, override);
|
||||
$scope.render();
|
||||
};
|
||||
|
||||
// Called from panel menu
|
||||
$scope.toggleLegend = function() {
|
||||
$scope.panel.legend.show = !$scope.panel.legend.show;
|
||||
$scope.get_data();
|
||||
};
|
||||
|
||||
$scope.legendValuesOptionChanged = function() {
|
||||
var legend = $scope.panel.legend;
|
||||
legend.values = legend.min || legend.max || legend.avg || legend.current || legend.total;
|
||||
$scope.render();
|
||||
};
|
||||
|
||||
$scope.exportCsv = function() {
|
||||
kbn.exportSeriesListToCsv($scope.seriesList);
|
||||
};
|
||||
|
||||
panelSrv.init($scope);
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
84
public/app/panels/graph/seriesOverridesCtrl.js
Normal file
84
public/app/panels/graph/seriesOverridesCtrl.js
Normal file
@@ -0,0 +1,84 @@
|
||||
define([
|
||||
'angular',
|
||||
'app',
|
||||
'lodash',
|
||||
], function(angular, app, _) {
|
||||
'use strict';
|
||||
|
||||
var module = angular.module('grafana.panels.graph', []);
|
||||
app.useModule(module);
|
||||
|
||||
module.controller('SeriesOverridesCtrl', function($scope) {
|
||||
$scope.overrideMenu = [];
|
||||
$scope.currentOverrides = [];
|
||||
$scope.override = $scope.override || {};
|
||||
|
||||
$scope.addOverrideOption = function(name, propertyName, values) {
|
||||
var option = {};
|
||||
option.text = name;
|
||||
option.propertyName = propertyName;
|
||||
option.index = $scope.overrideMenu.length;
|
||||
option.values = values;
|
||||
|
||||
option.submenu = _.map(values, function(value) {
|
||||
return { text: String(value), value: value };
|
||||
});
|
||||
|
||||
$scope.overrideMenu.push(option);
|
||||
};
|
||||
|
||||
$scope.setOverride = function(item, subItem) {
|
||||
$scope.override[item.propertyName] = subItem.value;
|
||||
|
||||
// automatically disable lines for this series and the fill bellow to series
|
||||
// can be removed by the user if they still want lines
|
||||
if (item.propertyName === 'fillBelowTo') {
|
||||
$scope.override['lines'] = false;
|
||||
$scope.addSeriesOverride({ alias: subItem.value, lines: false });
|
||||
}
|
||||
|
||||
$scope.updateCurrentOverrides();
|
||||
$scope.render();
|
||||
};
|
||||
|
||||
$scope.removeOverride = function(option) {
|
||||
delete $scope.override[option.propertyName];
|
||||
$scope.updateCurrentOverrides();
|
||||
$scope.render();
|
||||
};
|
||||
|
||||
$scope.getSeriesNames = function() {
|
||||
return _.map($scope.seriesList, function(series) {
|
||||
return series.alias;
|
||||
});
|
||||
};
|
||||
|
||||
$scope.updateCurrentOverrides = function() {
|
||||
$scope.currentOverrides = [];
|
||||
_.each($scope.overrideMenu, function(option) {
|
||||
var value = $scope.override[option.propertyName];
|
||||
if (_.isUndefined(value)) { return; }
|
||||
$scope.currentOverrides.push({
|
||||
name: option.text,
|
||||
propertyName: option.propertyName,
|
||||
value: String(value)
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
$scope.addOverrideOption('Bars', 'bars', [true, false]);
|
||||
$scope.addOverrideOption('Lines', 'lines', [true, false]);
|
||||
$scope.addOverrideOption('Line fill', 'fill', [0,1,2,3,4,5,6,7,8,9,10]);
|
||||
$scope.addOverrideOption('Line width', 'linewidth', [0,1,2,3,4,5,6,7,8,9,10]);
|
||||
$scope.addOverrideOption('Fill below to', 'fillBelowTo', $scope.getSeriesNames());
|
||||
$scope.addOverrideOption('Staircase line', 'steppedLine', [true, false]);
|
||||
$scope.addOverrideOption('Points', 'points', [true, false]);
|
||||
$scope.addOverrideOption('Points Radius', 'pointradius', [1,2,3,4,5]);
|
||||
$scope.addOverrideOption('Stack', 'stack', [true, false, 2, 3, 4, 5]);
|
||||
$scope.addOverrideOption('Y-axis', 'yaxis', [1, 2]);
|
||||
$scope.addOverrideOption('Z-index', 'zindex', [-1,-2,-3,0,1,2,3]);
|
||||
$scope.updateCurrentOverrides();
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
99
public/app/panels/graph/styleEditor.html
Normal file
99
public/app/panels/graph/styleEditor.html
Normal file
@@ -0,0 +1,99 @@
|
||||
<div class="editor-row">
|
||||
<div class="section">
|
||||
<h5>Chart Options</h5>
|
||||
<editor-opt-bool text="Bars" model="panel.bars" change="render()"></editor-opt-bool>
|
||||
<editor-opt-bool text="Lines" model="panel.lines" change="render()"></editor-opt-bool>
|
||||
<editor-opt-bool text="Points" model="panel.points" change="render()"></editor-opt-bool>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h5>Line options</h5>
|
||||
<div class="editor-option" ng-show="panel.lines">
|
||||
<label class="small">Line Fill</label>
|
||||
<select class="input-mini" ng-model="panel.fill" ng-options="f for f in [0,1,2,3,4,5,6,7,8,9,10]" ng-change="render()"></select>
|
||||
</div>
|
||||
<div class="editor-option" ng-show="panel.lines">
|
||||
<label class="small">Line Width</label>
|
||||
<select class="input-mini" ng-model="panel.linewidth" ng-options="f for f in [0,1,2,3,4,5,6,7,8,9,10]" ng-change="render()"></select>
|
||||
</div>
|
||||
<div class="editor-option" ng-show="panel.points">
|
||||
<label class="small">Point Radius</label>
|
||||
<select class="input-mini" ng-model="panel.pointradius" ng-options="f for f in [1,2,3,4,5,6,7,8,9,10]" ng-change="render()"></select>
|
||||
</div>
|
||||
<div class="editor-option">
|
||||
<label class="small">Null point mode<tip>Define how null values should be drawn</tip></label>
|
||||
<select class="input-medium" ng-model="panel.nullPointMode" ng-options="f for f in ['connected', 'null', 'null as zero']" ng-change="render()"></select>
|
||||
</div>
|
||||
|
||||
<editor-opt-bool text="Staircase line" model="panel.steppedLine" change="render()"></editor-opt-bool>
|
||||
</div>
|
||||
<div class="section">
|
||||
<h5>Multiple Series</h5>
|
||||
|
||||
<editor-opt-bool text="Stack" model="panel.stack" change="render()"></editor-opt-bool>
|
||||
<editor-opt-bool text="Percent" model="panel.percentage" change="render()" tip="Stack as a percentage of total"></editor-opt-bool>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h5>Rendering</h5>
|
||||
<div class="editor-option">
|
||||
<label class="small">Flot <tip>client side</tip></label>
|
||||
<input type="radio" class="input-small" ng-model="panel.renderer" value="flot" ng-change="get_data()" />
|
||||
</div>
|
||||
<div class="editor-option">
|
||||
<label class="small">Graphite PNG <tip>server side</tip></label>
|
||||
<input type="radio" class="input-small" ng-model="panel.renderer" value="png" ng-change="get_data()" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h5>Tooltip</h5>
|
||||
<editor-opt-bool
|
||||
text="All series" model="panel.tooltip.shared" change="render()"
|
||||
tip="Show all series on same tooltip and a x croshair to help follow all series">
|
||||
</editor-opt-bool>
|
||||
<div class="editor-option" ng-show="panel.stack">
|
||||
<label class="small">Stacked Values <tip>How should the values in stacked charts to be calculated?</tip></label>
|
||||
<select class="input-small" ng-model="panel.tooltip.value_type" ng-options="f for f in ['cumulative','individual']" ng-change="render()"></select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="editor-row">
|
||||
<div class="section">
|
||||
<h5>Series specific overrides <tip>Regex match example: /server[0-3]/i </tip></h5>
|
||||
<div>
|
||||
<div class="tight-form" ng-repeat="override in panel.seriesOverrides" ng-controller="SeriesOverridesCtrl">
|
||||
<ul class="tight-form-list">
|
||||
<li class="tight-form-item">
|
||||
<i class="fa fa-remove pointer" ng-click="removeSeriesOverride(override)"></i>
|
||||
</li>
|
||||
|
||||
<li class="tight-form-item">
|
||||
alias or regex
|
||||
</li>
|
||||
<li>
|
||||
<input type="text"
|
||||
ng-model="override.alias"
|
||||
bs-typeahead="getSeriesNames"
|
||||
ng-blur="render()"
|
||||
data-min-length=0 data-items=100
|
||||
class="input-medium tight-form-input" >
|
||||
</li>
|
||||
<li class="tight-form-item" ng-repeat="option in currentOverrides">
|
||||
<i class="pointer fa fa-remove" ng-click="removeOverride(option)"></i>
|
||||
{{option.name}}: {{option.value}}
|
||||
</li>
|
||||
|
||||
<li class="dropdown" dropdown-typeahead="overrideMenu" dropdown-typeahead-on-select="setOverride($item, $subItem)">
|
||||
</li>
|
||||
|
||||
</ul>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button class="btn btn-success" style="margin-top: 20px" ng-click="addSeriesOverride()">Add series override rule</button>
|
||||
</div>
|
||||
</div>
|
||||
189
public/app/panels/singlestat/editor.html
Normal file
189
public/app/panels/singlestat/editor.html
Normal file
@@ -0,0 +1,189 @@
|
||||
<div class="editor-row">
|
||||
<div class="section" style="margin-bottom: 20px">
|
||||
<div class="tight-form">
|
||||
<ul class="tight-form-list">
|
||||
<li class="tight-form-item" style="width: 80px">
|
||||
<strong>Big value</strong>
|
||||
</li>
|
||||
<li class="tight-form-item">
|
||||
Prefix
|
||||
</li>
|
||||
<li>
|
||||
<input type="text" class="input-small tight-form-input"
|
||||
ng-model="panel.prefix" ng-change="render()" ng-model-onblur>
|
||||
</li>
|
||||
<li class="tight-form-item">
|
||||
Value
|
||||
</li>
|
||||
<li>
|
||||
<select class="input-small tight-form-input" ng-model="panel.valueName" ng-options="f for f in ['min','max','avg', 'current', 'total']" ng-change="render()"></select>
|
||||
</li>
|
||||
<li class="tight-form-item">
|
||||
Postfix
|
||||
</li>
|
||||
<li>
|
||||
<input type="text" class="input-small tight-form-input last"
|
||||
ng-model="panel.postfix" ng-change="render()" ng-model-onblur>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
<div class="tight-form">
|
||||
<ul class="tight-form-list">
|
||||
<li class="tight-form-item" style="width: 80px">
|
||||
<strong>Font size</strong>
|
||||
</li>
|
||||
<li class="tight-form-item">
|
||||
Prefix
|
||||
</li>
|
||||
<li>
|
||||
<select class="input-small tight-form-input" ng-model="panel.prefixFontSize" ng-options="f for f in fontSizes" ng-change="render()"></select>
|
||||
</li>
|
||||
<li class="tight-form-item">
|
||||
Value
|
||||
</li>
|
||||
<li>
|
||||
<select class="input-small tight-form-input" ng-model="panel.valueFontSize" ng-options="f for f in fontSizes" ng-change="render()"></select>
|
||||
</li>
|
||||
<li class="tight-form-item">
|
||||
Postfix
|
||||
</li>
|
||||
<li>
|
||||
<select class="input-small tight-form-input last" ng-model="panel.postfixFontSize" ng-options="f for f in fontSizes" ng-change="render()"></select>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
<div class="tight-form">
|
||||
<ul class="tight-form-list">
|
||||
<li class="tight-form-item" style="width: 80px">
|
||||
<strong>Unit</strong>
|
||||
</li>
|
||||
<li class="dropdown" style="width: 119px;"
|
||||
ng-model="panel.format"
|
||||
dropdown-typeahead="unitFormats"
|
||||
dropdown-typeahead-on-select="setUnitFormat($subItem)">
|
||||
</li>
|
||||
<li class="tight-form-item">
|
||||
Decimals
|
||||
</li>
|
||||
<li>
|
||||
<input type="number" class="input-small tight-form-input" placeholder="auto" bs-tooltip="'Override automatic decimal precision for legend and tooltips'" data-placement="right"
|
||||
ng-model="panel.decimals" ng-change="render()" ng-model-onblur>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="editor-row">
|
||||
<div class="section" style="margin-bottom: 20px">
|
||||
<div class="tight-form">
|
||||
<ul class="tight-form-list">
|
||||
<li class="tight-form-item" style="width: 80px">
|
||||
<strong>Coloring</strong>
|
||||
</li>
|
||||
<li class="tight-form-item">
|
||||
Background
|
||||
<input class="cr1" id="panel.colorBackground" type="checkbox"
|
||||
ng-model="panel.colorBackground" ng-checked="panel.colorBackground" ng-change="render()">
|
||||
<label for="panel.colorBackground" class="cr1"></label>
|
||||
</li>
|
||||
<li class="tight-form-item">
|
||||
Value
|
||||
<input class="cr1" id="panel.colorValue" type="checkbox"
|
||||
ng-model="panel.colorValue" ng-checked="panel.colorValue" ng-change="render()">
|
||||
<label for="panel.colorValue" class="cr1"></label>
|
||||
</li>
|
||||
<li class="tight-form-item">
|
||||
Thresholds<tip>Comma seperated values</tip>
|
||||
</li>
|
||||
<li>
|
||||
<input type="text" class="input-large tight-form-input" ng-model="panel.thresholds" ng-blur="render()" placeholder="0,50,80"></input>
|
||||
</li>
|
||||
<li class="tight-form-item">
|
||||
Colors
|
||||
</li>
|
||||
<li class="tight-form-item">
|
||||
<spectrum-picker ng-model="panel.colors[0]" ng-change="render()" ></spectrum-picker>
|
||||
<spectrum-picker ng-model="panel.colors[1]" ng-change="render()" ></spectrum-picker>
|
||||
<spectrum-picker ng-model="panel.colors[2]" ng-change="render()" ></spectrum-picker>
|
||||
</li>
|
||||
<li class="tight-form-item last">
|
||||
<a class="pointer" ng-click="invertColorOrder()">invert order</a>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="editor-row">
|
||||
<div class="section" style="margin-bottom: 20px">
|
||||
<div class="tight-form">
|
||||
<ul class="tight-form-list">
|
||||
<li class="tight-form-item" style="width: 80px">
|
||||
<strong>Spark lines</strong>
|
||||
</li>
|
||||
<li class="tight-form-item">
|
||||
Show
|
||||
<input class="cr1" id="panel.sparkline.show" type="checkbox"
|
||||
ng-model="panel.sparkline.show" ng-checked="panel.sparkline.show" ng-change="render()">
|
||||
<label for="panel.sparkline.show" class="cr1"></label>
|
||||
</li>
|
||||
<li class="tight-form-item">
|
||||
Background mode
|
||||
<input class="cr1" id="panel.sparkline.full" type="checkbox"
|
||||
ng-model="panel.sparkline.full" ng-checked="panel.sparkline.full" ng-change="render()">
|
||||
<label for="panel.sparkline.full" class="cr1"></label>
|
||||
</li>
|
||||
<li class="tight-form-item">
|
||||
Line Color
|
||||
</li>
|
||||
<li class="tight-form-item">
|
||||
<spectrum-picker ng-model="panel.sparkline.lineColor" ng-change="render()" ></spectrum-picker>
|
||||
</li>
|
||||
<li class="tight-form-item">
|
||||
Fill Color
|
||||
</li>
|
||||
<li class="tight-form-item">
|
||||
<spectrum-picker ng-model="panel.sparkline.fillColor" ng-change="render()" ></spectrum-picker>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="editor-row">
|
||||
<div class="section" style="margin-bottom: 20px">
|
||||
<div class="tight-form">
|
||||
<ul class="tight-form-list">
|
||||
<li class="tight-form-item">
|
||||
<strong>Value to text mapping</strong>
|
||||
</li>
|
||||
<li class="tight-form-item" ng-repeat-start="map in panel.valueMaps">
|
||||
<i class="fa fa-remove pointer" ng-click="removeValueMap(map)"></i>
|
||||
</li>
|
||||
<li>
|
||||
<input type="text" ng-model="map.value" placeholder="value" class="input-mini tight-form-input" ng-blur="render()">
|
||||
</li>
|
||||
<li class="tight-form-item">
|
||||
<i class="fa fa-arrow-right"></i>
|
||||
</li>
|
||||
<li ng-repeat-end>
|
||||
<input type="text" placeholder="text" ng-model="map.text" class="input-mini tight-form-input" ng-blur="render()">
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<a class="pointer tight-form-item" ng-click="addValueMap();">
|
||||
<i class="fa fa-plus"></i>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
</ul>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
4
public/app/panels/singlestat/module.html
Normal file
4
public/app/panels/singlestat/module.html
Normal file
@@ -0,0 +1,4 @@
|
||||
<grafana-panel>
|
||||
<div class="singlestat-panel" singlestat-panel></div>
|
||||
<div class="clearfix"></div>
|
||||
</grafana-panel>
|
||||
235
public/app/panels/singlestat/module.js
Normal file
235
public/app/panels/singlestat/module.js
Normal file
@@ -0,0 +1,235 @@
|
||||
define([
|
||||
'angular',
|
||||
'app',
|
||||
'lodash',
|
||||
'components/timeSeries',
|
||||
'kbn',
|
||||
'components/panelmeta',
|
||||
'./singleStatPanel',
|
||||
],
|
||||
function (angular, app, _, TimeSeries, kbn, PanelMeta) {
|
||||
'use strict';
|
||||
|
||||
var module = angular.module('grafana.panels.singlestat');
|
||||
app.useModule(module);
|
||||
|
||||
module.directive('grafanaPanelSinglestat', function() {
|
||||
return {
|
||||
controller: 'SingleStatCtrl',
|
||||
templateUrl: 'app/panels/singlestat/module.html',
|
||||
};
|
||||
});
|
||||
|
||||
module.controller('SingleStatCtrl', function($scope, panelSrv, panelHelper) {
|
||||
|
||||
$scope.panelMeta = new PanelMeta({
|
||||
panelName: 'Singlestat',
|
||||
editIcon: "fa fa-dashboard",
|
||||
fullscreen: true,
|
||||
metricsEditor: true
|
||||
});
|
||||
|
||||
$scope.fontSizes = ['20%', '30%','50%','70%','80%','100%', '110%', '120%', '150%', '170%', '200%'];
|
||||
|
||||
$scope.panelMeta.addEditorTab('Options', 'app/panels/singlestat/editor.html');
|
||||
$scope.panelMeta.addEditorTab('Time range', 'app/features/panel/partials/panelTime.html');
|
||||
|
||||
// Set and populate defaults
|
||||
var _d = {
|
||||
links: [],
|
||||
maxDataPoints: 100,
|
||||
interval: null,
|
||||
targets: [{}],
|
||||
cacheTimeout: null,
|
||||
format: 'none',
|
||||
prefix: '',
|
||||
postfix: '',
|
||||
nullText: null,
|
||||
valueMaps: [
|
||||
{ value: 'null', op: '=', text: 'N/A' }
|
||||
],
|
||||
nullPointMode: 'connected',
|
||||
valueName: 'avg',
|
||||
prefixFontSize: '50%',
|
||||
valueFontSize: '80%',
|
||||
postfixFontSize: '50%',
|
||||
thresholds: '',
|
||||
colorBackground: false,
|
||||
colorValue: false,
|
||||
colors: ["rgba(245, 54, 54, 0.9)", "rgba(237, 129, 40, 0.89)", "rgba(50, 172, 45, 0.97)"],
|
||||
sparkline: {
|
||||
show: false,
|
||||
full: false,
|
||||
lineColor: 'rgb(31, 120, 193)',
|
||||
fillColor: 'rgba(31, 118, 189, 0.18)',
|
||||
}
|
||||
};
|
||||
|
||||
_.defaults($scope.panel, _d);
|
||||
$scope.unitFormats = kbn.getUnitFormats();
|
||||
|
||||
$scope.setUnitFormat = function(subItem) {
|
||||
$scope.panel.format = subItem.value;
|
||||
$scope.render();
|
||||
};
|
||||
|
||||
$scope.init = function() {
|
||||
panelSrv.init($scope);
|
||||
};
|
||||
|
||||
$scope.refreshData = function(datasource) {
|
||||
panelHelper.updateTimeRange($scope);
|
||||
|
||||
return panelHelper.issueMetricQuery($scope, datasource)
|
||||
.then($scope.dataHandler, function(err) {
|
||||
$scope.series = [];
|
||||
$scope.render();
|
||||
throw err;
|
||||
});
|
||||
};
|
||||
|
||||
$scope.loadSnapshot = function(snapshotData) {
|
||||
panelHelper.updateTimeRange($scope);
|
||||
$scope.dataHandler(snapshotData);
|
||||
};
|
||||
|
||||
$scope.dataHandler = function(results) {
|
||||
$scope.series = _.map(results.data, $scope.seriesHandler);
|
||||
$scope.render();
|
||||
};
|
||||
|
||||
$scope.seriesHandler = function(seriesData) {
|
||||
var series = new TimeSeries({
|
||||
datapoints: seriesData.datapoints,
|
||||
alias: seriesData.target,
|
||||
});
|
||||
|
||||
series.flotpairs = series.getFlotPairs($scope.panel.nullPointMode);
|
||||
|
||||
return series;
|
||||
};
|
||||
|
||||
$scope.setColoring = function(options) {
|
||||
if (options.background) {
|
||||
$scope.panel.colorValue = false;
|
||||
$scope.panel.colors = ['rgba(71, 212, 59, 0.4)', 'rgba(245, 150, 40, 0.73)', 'rgba(225, 40, 40, 0.59)'];
|
||||
}
|
||||
else {
|
||||
$scope.panel.colorBackground = false;
|
||||
$scope.panel.colors = ['rgba(50, 172, 45, 0.97)', 'rgba(237, 129, 40, 0.89)', 'rgba(245, 54, 54, 0.9)'];
|
||||
}
|
||||
$scope.render();
|
||||
};
|
||||
|
||||
$scope.invertColorOrder = function() {
|
||||
var tmp = $scope.panel.colors[0];
|
||||
$scope.panel.colors[0] = $scope.panel.colors[2];
|
||||
$scope.panel.colors[2] = tmp;
|
||||
$scope.render();
|
||||
};
|
||||
|
||||
$scope.getDecimalsForValue = function(value) {
|
||||
if ($scope.panel.decimals) {
|
||||
return { decimals: $scope.panel.decimals, scaledDecimals: null };
|
||||
}
|
||||
|
||||
var delta = value / 2;
|
||||
var dec = -Math.floor(Math.log(delta) / Math.LN10);
|
||||
|
||||
var magn = Math.pow(10, -dec),
|
||||
norm = delta / magn, // norm is between 1.0 and 10.0
|
||||
size;
|
||||
|
||||
if (norm < 1.5) {
|
||||
size = 1;
|
||||
} else if (norm < 3) {
|
||||
size = 2;
|
||||
// special case for 2.5, requires an extra decimal
|
||||
if (norm > 2.25) {
|
||||
size = 2.5;
|
||||
++dec;
|
||||
}
|
||||
} else if (norm < 7.5) {
|
||||
size = 5;
|
||||
} else {
|
||||
size = 10;
|
||||
}
|
||||
|
||||
size *= magn;
|
||||
|
||||
// reduce starting decimals if not needed
|
||||
if (Math.floor(value) === value) { dec = 0; }
|
||||
|
||||
var result = {};
|
||||
result.decimals = Math.max(0, dec);
|
||||
result.scaledDecimals = result.decimals - Math.floor(Math.log(size) / Math.LN10) + 2;
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
$scope.render = function() {
|
||||
var data = {};
|
||||
|
||||
if (!$scope.series || $scope.series.length === 0) {
|
||||
data.flotpairs = [];
|
||||
data.mainValue = Number.NaN;
|
||||
data.mainValueFormated = $scope.getFormatedValue(null);
|
||||
}
|
||||
else {
|
||||
var series = $scope.series[0];
|
||||
data.mainValue = series.stats[$scope.panel.valueName];
|
||||
data.mainValueFormated = $scope.getFormatedValue(data.mainValue);
|
||||
data.flotpairs = series.flotpairs;
|
||||
}
|
||||
|
||||
data.thresholds = $scope.panel.thresholds.split(',').map(function(strVale) {
|
||||
return Number(strVale.trim());
|
||||
});
|
||||
|
||||
data.colorMap = $scope.panel.colors;
|
||||
|
||||
$scope.data = data;
|
||||
$scope.$broadcast('render');
|
||||
};
|
||||
|
||||
$scope.getFormatedValue = function(mainValue) {
|
||||
|
||||
// first check value to text mappings
|
||||
for(var i = 0; i < $scope.panel.valueMaps.length; i++) {
|
||||
var map = $scope.panel.valueMaps[i];
|
||||
// special null case
|
||||
if (map.value === 'null') {
|
||||
if (mainValue === null || mainValue === void 0) {
|
||||
return map.text;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
// value/number to text mapping
|
||||
var value = parseFloat(map.value);
|
||||
if (value === mainValue) {
|
||||
return map.text;
|
||||
}
|
||||
}
|
||||
|
||||
if (mainValue === null || mainValue === void 0) {
|
||||
return "no value";
|
||||
}
|
||||
|
||||
var decimalInfo = $scope.getDecimalsForValue(mainValue);
|
||||
var formatFunc = kbn.valueFormats[$scope.panel.format];
|
||||
return formatFunc(mainValue, decimalInfo.decimals, decimalInfo.scaledDecimals);
|
||||
};
|
||||
|
||||
$scope.removeValueMap = function(map) {
|
||||
var index = _.indexOf($scope.panel.valueMaps, map);
|
||||
$scope.panel.valueMaps.splice(index, 1);
|
||||
$scope.render();
|
||||
};
|
||||
|
||||
$scope.addValueMap = function() {
|
||||
$scope.panel.valueMaps.push({value: '', op: '=', text: '' });
|
||||
};
|
||||
|
||||
$scope.init();
|
||||
});
|
||||
});
|
||||
210
public/app/panels/singlestat/singleStatPanel.js
Normal file
210
public/app/panels/singlestat/singleStatPanel.js
Normal file
@@ -0,0 +1,210 @@
|
||||
define([
|
||||
'angular',
|
||||
'app',
|
||||
'lodash',
|
||||
'jquery',
|
||||
'jquery.flot',
|
||||
],
|
||||
function (angular, app, _, $) {
|
||||
'use strict';
|
||||
|
||||
var module = angular.module('grafana.panels.singlestat', []);
|
||||
app.useModule(module);
|
||||
|
||||
module.directive('singlestatPanel', function($location, linkSrv, $timeout, templateSrv) {
|
||||
|
||||
return {
|
||||
link: function(scope, elem) {
|
||||
var data, panel;
|
||||
var $panelContainer = elem.parents('.panel-container');
|
||||
|
||||
scope.$on('render', function() {
|
||||
render();
|
||||
});
|
||||
|
||||
function setElementHeight() {
|
||||
try {
|
||||
var height = scope.height || panel.height || scope.row.height;
|
||||
if (_.isString(height)) {
|
||||
height = parseInt(height.replace('px', ''), 10);
|
||||
}
|
||||
|
||||
height -= 5; // padding
|
||||
height -= panel.title ? 24 : 9; // subtract panel title bar
|
||||
|
||||
elem.css('height', height + 'px');
|
||||
|
||||
return true;
|
||||
} catch(e) { // IE throws errors sometimes
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function applyColoringThresholds(value, valueString) {
|
||||
if (!panel.colorValue) {
|
||||
return valueString;
|
||||
}
|
||||
|
||||
var color = getColorForValue(value);
|
||||
if (color) {
|
||||
return '<span style="color:' + color + '">'+ valueString + '</span>';
|
||||
}
|
||||
|
||||
return valueString;
|
||||
}
|
||||
|
||||
function getColorForValue(value) {
|
||||
for (var i = data.thresholds.length - 1; i >= 0 ; i--) {
|
||||
if (value >= data.thresholds[i]) {
|
||||
return data.colorMap[i];
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function getSpan(className, fontSize, value) {
|
||||
value = templateSrv.replace(value);
|
||||
return '<span class="' + className + '" style="font-size:' + fontSize + '">' +
|
||||
value + '</span>';
|
||||
}
|
||||
|
||||
function getBigValueHtml() {
|
||||
var body = '<div class="singlestat-panel-value-container">';
|
||||
|
||||
if (panel.prefix) { body += getSpan('singlestat-panel-prefix', panel.prefixFontSize, scope.panel.prefix); }
|
||||
|
||||
var value = applyColoringThresholds(data.mainValue, data.mainValueFormated);
|
||||
body += getSpan('singlestat-panel-value', panel.valueFontSize, value);
|
||||
|
||||
if (panel.postfix) { body += getSpan('singlestat-panel-postfix', panel.postfixFontSize, panel.postfix); }
|
||||
|
||||
body += '</div>';
|
||||
|
||||
return body;
|
||||
}
|
||||
|
||||
function addSparkline() {
|
||||
var panel = scope.panel;
|
||||
var width = elem.width() + 20;
|
||||
var height = elem.height() || 100;
|
||||
|
||||
var plotCanvas = $('<div></div>');
|
||||
var plotCss = {};
|
||||
plotCss.position = 'absolute';
|
||||
|
||||
if (panel.sparkline.full) {
|
||||
plotCss.bottom = '5px';
|
||||
plotCss.left = '-5px';
|
||||
plotCss.width = (width - 10) + 'px';
|
||||
plotCss.height = (height - 45) + 'px';
|
||||
}
|
||||
else {
|
||||
plotCss.bottom = "0px";
|
||||
plotCss.left = "-5px";
|
||||
plotCss.width = (width - 10) + 'px';
|
||||
plotCss.height = Math.floor(height * 0.25) + "px";
|
||||
}
|
||||
|
||||
plotCanvas.css(plotCss);
|
||||
|
||||
var options = {
|
||||
legend: { show: false },
|
||||
series: {
|
||||
lines: {
|
||||
show: true,
|
||||
fill: 1,
|
||||
lineWidth: 1,
|
||||
fillColor: panel.sparkline.fillColor,
|
||||
},
|
||||
},
|
||||
yaxes: { show: false },
|
||||
xaxis: {
|
||||
show: false,
|
||||
mode: "time",
|
||||
min: scope.range.from.getTime(),
|
||||
max: scope.range.to.getTime(),
|
||||
},
|
||||
grid: { hoverable: false, show: false },
|
||||
};
|
||||
|
||||
elem.append(plotCanvas);
|
||||
|
||||
var plotSeries = {
|
||||
data: data.flotpairs,
|
||||
color: panel.sparkline.lineColor
|
||||
};
|
||||
|
||||
$.plot(plotCanvas, [plotSeries], options);
|
||||
}
|
||||
|
||||
function render() {
|
||||
if (!scope.data) { return; }
|
||||
|
||||
data = scope.data;
|
||||
panel = scope.panel;
|
||||
|
||||
setElementHeight();
|
||||
|
||||
var body = getBigValueHtml();
|
||||
|
||||
if (panel.colorBackground && !isNaN(data.mainValue)) {
|
||||
var color = getColorForValue(data.mainValue);
|
||||
if (color) {
|
||||
$panelContainer.css('background-color', color);
|
||||
if (scope.fullscreen) {
|
||||
elem.css('background-color', color);
|
||||
} else {
|
||||
elem.css('background-color', '');
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$panelContainer.css('background-color', '');
|
||||
elem.css('background-color', '');
|
||||
}
|
||||
|
||||
elem.html(body);
|
||||
|
||||
if (panel.sparkline.show) {
|
||||
addSparkline();
|
||||
}
|
||||
|
||||
elem.toggleClass('pointer', panel.links.length > 0);
|
||||
}
|
||||
|
||||
// drilldown link tooltip
|
||||
var drilldownTooltip = $('<div id="tooltip" class="">gello</div>"');
|
||||
|
||||
elem.mouseleave(function() {
|
||||
if (panel.links.length === 0) { return;}
|
||||
drilldownTooltip.detach();
|
||||
});
|
||||
|
||||
elem.click(function() {
|
||||
if (panel.links.length === 0) { return; }
|
||||
|
||||
var linkInfo = linkSrv.getPanelLinkAnchorInfo(panel.links[0]);
|
||||
if (linkInfo.href[0] === '#') { linkInfo.href = linkInfo.href.substring(1); }
|
||||
|
||||
if (linkInfo.href.indexOf('http') === 0) {
|
||||
window.location.href = linkInfo.href;
|
||||
} else {
|
||||
$timeout(function() {
|
||||
$location.url(linkInfo.href);
|
||||
});
|
||||
}
|
||||
|
||||
drilldownTooltip.detach();
|
||||
});
|
||||
|
||||
elem.mousemove(function(e) {
|
||||
if (panel.links.length === 0) { return;}
|
||||
|
||||
drilldownTooltip.text('click to go to: ' + panel.links[0].title);
|
||||
|
||||
drilldownTooltip.place_tt(e.pageX+20, e.pageY-15);
|
||||
});
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
});
|
||||
17
public/app/panels/text/editor.html
Normal file
17
public/app/panels/text/editor.html
Normal file
@@ -0,0 +1,17 @@
|
||||
<div>
|
||||
<div class="row-fluid">
|
||||
<div class="span4">
|
||||
<label class="small">Mode</label> <select class="input-medium" ng-model="panel.mode" ng-options="f for f in ['html','markdown','text']"></select>
|
||||
</div>
|
||||
<div class="span2" ng-show="panel.mode == 'text'">
|
||||
<label class="small">Font Size</label> <select class="input-mini" ng-model="panel.style['font-size']" ng-options="f for f in ['6pt','7pt','8pt','10pt','12pt','14pt','16pt','18pt','20pt','24pt','28pt','32pt','36pt','42pt','48pt','52pt','60pt','72pt']"></select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<label class=small>Content
|
||||
<span ng-show="panel.mode == 'markdown'">(This area uses <a target="_blank" href="http://en.wikipedia.org/wiki/Markdown">Markdown</a>. HTML is not supported)</span>
|
||||
</label>
|
||||
|
||||
<textarea ng-model="panel.content" rows="20" style="width:95%" ng-change="render()" ng-model-onblur>
|
||||
</textarea>
|
||||
</div>
|
||||
1454
public/app/panels/text/lib/showdown.js
Normal file
1454
public/app/panels/text/lib/showdown.js
Normal file
File diff suppressed because it is too large
Load Diff
3
public/app/panels/text/module.html
Normal file
3
public/app/panels/text/module.html
Normal file
@@ -0,0 +1,3 @@
|
||||
<grafana-panel>
|
||||
<p ng-bind-html="content" ng-show="content"></p>
|
||||
</grafana-panel>
|
||||
111
public/app/panels/text/module.js
Normal file
111
public/app/panels/text/module.js
Normal file
@@ -0,0 +1,111 @@
|
||||
define([
|
||||
'angular',
|
||||
'app',
|
||||
'lodash',
|
||||
'require',
|
||||
'components/panelmeta',
|
||||
],
|
||||
function (angular, app, _, require, PanelMeta) {
|
||||
'use strict';
|
||||
|
||||
var converter;
|
||||
|
||||
var module = angular.module('grafana.panels.text', []);
|
||||
app.useModule(module);
|
||||
|
||||
module.directive('grafanaPanelText', function() {
|
||||
return {
|
||||
controller: 'TextPanelCtrl',
|
||||
templateUrl: 'app/panels/text/module.html',
|
||||
};
|
||||
});
|
||||
|
||||
module.controller('TextPanelCtrl', function($scope, templateSrv, $sce, panelSrv) {
|
||||
|
||||
$scope.panelMeta = new PanelMeta({
|
||||
panelName: 'Text',
|
||||
editIcon: "fa fa-text-width",
|
||||
fullscreen: true,
|
||||
});
|
||||
|
||||
$scope.panelMeta.addEditorTab('Edit text', 'app/panels/text/editor.html');
|
||||
|
||||
// Set and populate defaults
|
||||
var _d = {
|
||||
title : 'default title',
|
||||
mode : "markdown", // 'html', 'markdown', 'text'
|
||||
content : "",
|
||||
style: {},
|
||||
};
|
||||
|
||||
_.defaults($scope.panel, _d);
|
||||
|
||||
$scope.init = function() {
|
||||
panelSrv.init($scope);
|
||||
$scope.ready = false;
|
||||
$scope.render();
|
||||
};
|
||||
|
||||
$scope.refreshData = function() {
|
||||
$scope.panelMeta.loading = false;
|
||||
$scope.render();
|
||||
};
|
||||
|
||||
$scope.render = function() {
|
||||
if ($scope.panel.mode === 'markdown') {
|
||||
$scope.renderMarkdown($scope.panel.content);
|
||||
}
|
||||
else if ($scope.panel.mode === 'html') {
|
||||
$scope.updateContent($scope.panel.content);
|
||||
}
|
||||
else if ($scope.panel.mode === 'text') {
|
||||
$scope.renderText($scope.panel.content);
|
||||
}
|
||||
};
|
||||
|
||||
$scope.renderText = function(content) {
|
||||
content = content
|
||||
.replace(/&/g, '&')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/</g, '<')
|
||||
.replace(/\n/g, '<br/>');
|
||||
|
||||
$scope.updateContent(content);
|
||||
};
|
||||
|
||||
$scope.renderMarkdown = function(content) {
|
||||
var text = content
|
||||
.replace(/&/g, '&')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/</g, '<');
|
||||
|
||||
if (converter) {
|
||||
$scope.updateContent(converter.makeHtml(text));
|
||||
}
|
||||
else {
|
||||
require(['./lib/showdown'], function (Showdown) {
|
||||
converter = new Showdown.converter();
|
||||
$scope.updateContent(converter.makeHtml(text));
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
$scope.updateContent = function(html) {
|
||||
try {
|
||||
$scope.content = $sce.trustAsHtml(templateSrv.replace(html));
|
||||
} catch(e) {
|
||||
console.log('Text panel error: ', e);
|
||||
$scope.content = $sce.trustAsHtml(html);
|
||||
}
|
||||
|
||||
if(!$scope.$$phase) {
|
||||
$scope.$digest();
|
||||
}
|
||||
};
|
||||
|
||||
$scope.openEditor = function() {
|
||||
};
|
||||
|
||||
$scope.init();
|
||||
});
|
||||
});
|
||||
81
public/app/panels/timepicker/custom.html
Normal file
81
public/app/panels/timepicker/custom.html
Normal file
@@ -0,0 +1,81 @@
|
||||
<div class="gf-box-header">
|
||||
<div class="gf-box-title">
|
||||
<i class="fa fa-clock-o"></i>
|
||||
Custom time range
|
||||
</div>
|
||||
<button class="gf-box-header-close-btn" ng-click="dismiss();">
|
||||
<i class="fa fa-remove"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="gf-box-body">
|
||||
<style>
|
||||
.timepicker-to-column {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.timepicker-input input {
|
||||
outline: 0 !important;
|
||||
border: 0px !important;
|
||||
-webkit-box-shadow: 0;
|
||||
-moz-box-shadow: 0;
|
||||
box-shadow: 0;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.timepicker-input input::-webkit-outer-spin-button,
|
||||
.timepicker-input input::-webkit-inner-spin-button {
|
||||
-webkit-appearance: none;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
input.timepicker-date {
|
||||
width: 90px;
|
||||
}
|
||||
input.timepicker-hms {
|
||||
width: 20px;
|
||||
}
|
||||
input.timepicker-ms {
|
||||
width: 25px;
|
||||
}
|
||||
div.timepicker-now {
|
||||
float: right;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="timepicker form-horizontal">
|
||||
<form name="timeForm" style="margin-bottom: 0">
|
||||
|
||||
<div class="timepicker-from-column">
|
||||
<label class="small">From</label>
|
||||
<div class="fake-input timepicker-input">
|
||||
<input class="timepicker-date" type="text" ng-change="validate(temptime)" ng-model="temptime.from.date" data-date-format="yyyy-mm-dd" required bs-datepicker />@
|
||||
<input class="timepicker-hms" type="text" maxlength="2" ng-change="validate(temptime)" ng-model="temptime.from.hour" required ng-pattern="patterns.hour" onClick="this.select();"/>:
|
||||
<input class="timepicker-hms" type="text" maxlength="2" ng-change="validate(temptime)" ng-model="temptime.from.minute" required ng-pattern="patterns.minute" onClick="this.select();"/>:
|
||||
<input class="timepicker-hms" type="text" maxlength="2" ng-change="validate(temptime)" ng-model="temptime.from.second" required ng-pattern="patterns.second" onClick="this.select();"/>.
|
||||
<input class="timepicker-ms" type="text" maxlength="3" ng-change="validate(temptime)" ng-model="temptime.from.millisecond" required ng-pattern="patterns.millisecond" onClick="this.select();"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="timepicker-to-column">
|
||||
|
||||
<label class="small">To (<a class="link" ng-class="{'strong':temptime.now}" ng-click="setNow();temptime.now=true">set now</a>)</label>
|
||||
|
||||
<div class="fake-input timepicker-input">
|
||||
<div ng-hide="temptime.now">
|
||||
<input class="timepicker-date" type="text" ng-change="validate(temptime)" ng-model="temptime.to.date" data-date-format="yyyy-mm-dd" required bs-datepicker />@
|
||||
<input class="timepicker-hms" type="text" maxlength="2" ng-change="validate(temptime)" ng-model="temptime.to.hour" required ng-pattern="patterns.hour" onClick="this.select();"/>:
|
||||
<input class="timepicker-hms" type="text" maxlength="2" ng-change="validate(temptime)" ng-model="temptime.to.minute" required ng-pattern="patterns.minute" onClick="this.select();"/>:
|
||||
<input class="timepicker-hms" type="text" maxlength="2" ng-change="validate(temptime)" ng-model="temptime.to.second" required ng-pattern="patterns.second" onClick="this.select();"/>.
|
||||
<input class="timepicker-ms" type="text" maxlength="3" ng-change="validate(temptime)" ng-model="temptime.to.millisecond" required ng-pattern="patterns.millisecond" onClick="this.select();"/>
|
||||
</div>
|
||||
<span type="text" ng-show="temptime.now" ng-disabled="temptime.now">  <i class="pointer fa fa-remove" ng-click="setNow();temptime.now=false;"></i> Right Now <input type="text" name="dummy" style="visibility:hidden" /></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<br>
|
||||
<button ng-click="setAbsoluteTimeFilter(validate(temptime));dismiss();" ng-disabled="!timeForm.$valid" class="btn btn-success">Apply</button>
|
||||
<span class="" ng-hide="input.$valid">Invalid date or range</span>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
49
public/app/panels/timepicker/editor.html
Normal file
49
public/app/panels/timepicker/editor.html
Normal file
@@ -0,0 +1,49 @@
|
||||
<div class="editor-row">
|
||||
<div class="section">
|
||||
<div>
|
||||
<div class="tight-form">
|
||||
<ul class="tight-form-list">
|
||||
<li class="tight-form-item" style="width: 148px">
|
||||
<strong>Relative time options</strong></small>
|
||||
</li>
|
||||
<li>
|
||||
<input type="text" class="input-xlarge tight-form-input"
|
||||
ng-model="panel.time_options" array-join>
|
||||
</li>
|
||||
<li class="tight-form-item">
|
||||
Until
|
||||
</li>
|
||||
<li class="tight-form-item">
|
||||
now-
|
||||
</li>
|
||||
<li>
|
||||
<input type="text" class="input-mini tight-form-input last"
|
||||
ng-model="panel.nowDelay" placeholder="0m"
|
||||
valid-time-span
|
||||
bs-tooltip="'Enter 1m to ignore the last minute (because it can contain incomplete metrics)'"
|
||||
data-placement="right">
|
||||
</li>
|
||||
</ul>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
<div class="tight-form">
|
||||
<ul class="tight-form-list">
|
||||
<li class="tight-form-item" style="width: 148px">
|
||||
<strong>Auto-refresh options</strong>
|
||||
</li>
|
||||
<li>
|
||||
<input type="text" class="input-xlarge tight-form-input"
|
||||
ng-model="panel.refresh_intervals" array-join>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p>
|
||||
<br>
|
||||
<i class="fa fa-info-circle"></i>
|
||||
For these changes to fully take effect save and reload the dashboard.
|
||||
</i>
|
||||
</div>
|
||||
</div>
|
||||
57
public/app/panels/timepicker/module.html
Normal file
57
public/app/panels/timepicker/module.html
Normal file
@@ -0,0 +1,57 @@
|
||||
<div ng-controller='timepicker' ng-init="init()">
|
||||
<style>
|
||||
.timepicker-timestring {
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.timepicker-dropdown {
|
||||
margin: 0px !important;
|
||||
border: 0px !important;
|
||||
}
|
||||
</style>
|
||||
<form name="input" style="margin:0">
|
||||
<ul class="nav timepicker-dropdown">
|
||||
|
||||
<li class="grafana-menu-zoom-out">
|
||||
<a class='small' ng-click='zoom(2)'>
|
||||
Zoom Out
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li class="dropdown">
|
||||
|
||||
<a class="dropdown-toggle timepicker-dropdown" data-toggle="dropdown" href="" bs-tooltip="time.tooltip" data-placement="bottom" ng-click="dismiss();">
|
||||
<i class="fa fa-clock-o"></i>
|
||||
<span ng-bind="time.rangeString"></span>
|
||||
<span ng-show="dashboard.refresh" class="text-warning">refreshed every {{dashboard.refresh}} </span>
|
||||
<i class="fa fa-caret-down"></i>
|
||||
</a>
|
||||
|
||||
<ul class="dropdown-menu">
|
||||
<!-- Relative time options -->
|
||||
<li bindonce ng-repeat='timespan in panel.time_options track by $index'>
|
||||
<a ng-click="setRelativeFilter(timespan)" bo-text="'Last ' + timespan"></a>
|
||||
</li>
|
||||
|
||||
<!-- Auto refresh submenu -->
|
||||
<li class="dropdown-submenu">
|
||||
<a href="#">Auto-Refresh</a>
|
||||
<ul class="dropdown-menu">
|
||||
<li>
|
||||
<a ng-click="timeSrv.set_interval(false)">Off</a>
|
||||
</li>
|
||||
<li bindonce ng-repeat="interval in panel.refresh_intervals track by $index">
|
||||
<a ng-click="timeSrv.set_interval(interval)" bo-text="'Every ' + interval"></a>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li><a ng-click="customTime()">Custom</a></li>
|
||||
</ul>
|
||||
|
||||
</li>
|
||||
<li ng-show="!dashboard.refresh" class="grafana-menu-refresh">
|
||||
<a ng-click="timeSrv.refreshDashboard()"><i class="fa fa-refresh"></i></a>
|
||||
</li>
|
||||
</ul>
|
||||
</form>
|
||||
</div>
|
||||
242
public/app/panels/timepicker/module.js
Normal file
242
public/app/panels/timepicker/module.js
Normal file
@@ -0,0 +1,242 @@
|
||||
/*
|
||||
|
||||
## Timepicker2
|
||||
|
||||
### Parameters
|
||||
* mode :: The default mode of the panel. Options: 'relative', 'absolute' 'since' Default: 'relative'
|
||||
* time_options :: An array of possible time options. Default: ['5m','15m','1h','6h','12h','24h','2d','7d','30d']
|
||||
* timespan :: The default options selected for the relative view. Default: '15m'
|
||||
* timefield :: The field in which time is stored in the document.
|
||||
* refresh: Object containing refresh parameters
|
||||
* enable :: true/false, enable auto refresh by default. Default: false
|
||||
* interval :: Seconds between auto refresh. Default: 30
|
||||
* min :: The lowest interval a user may set
|
||||
*/
|
||||
define([
|
||||
'angular',
|
||||
'app',
|
||||
'lodash',
|
||||
'moment',
|
||||
'kbn'
|
||||
],
|
||||
function (angular, app, _, moment, kbn) {
|
||||
'use strict';
|
||||
|
||||
var module = angular.module('grafana.panels.timepicker', []);
|
||||
app.useModule(module);
|
||||
|
||||
module.controller('timepicker', function($scope, $rootScope, timeSrv) {
|
||||
|
||||
$scope.panelMeta = {
|
||||
status : "Stable",
|
||||
description : ""
|
||||
};
|
||||
|
||||
// Set and populate defaults
|
||||
var _d = {
|
||||
status : "Stable",
|
||||
time_options : ['5m','15m','1h','6h','12h','24h','2d','7d','30d'],
|
||||
refresh_intervals : ['5s','10s','30s','1m','5m','15m','30m','1h','2h','1d'],
|
||||
};
|
||||
|
||||
_.defaults($scope.panel,_d);
|
||||
|
||||
// ng-pattern regexs
|
||||
$scope.patterns = {
|
||||
date: /^[0-9]{2}\/[0-9]{2}\/[0-9]{4}$/,
|
||||
hour: /^([01]?[0-9]|2[0-3])$/,
|
||||
minute: /^[0-5][0-9]$/,
|
||||
second: /^[0-5][0-9]$/,
|
||||
millisecond: /^[0-9]*$/
|
||||
};
|
||||
|
||||
$scope.timeSrv = timeSrv;
|
||||
|
||||
$scope.$on('refresh', function() {
|
||||
$scope.init();
|
||||
});
|
||||
|
||||
$scope.init = function() {
|
||||
var time = timeSrv.timeRange(true);
|
||||
$scope.panel.now = false;
|
||||
|
||||
var unparsed = timeSrv.timeRange(false);
|
||||
if (_.isString(unparsed.to) && unparsed.to.indexOf('now') === 0) {
|
||||
$scope.panel.now = true;
|
||||
}
|
||||
|
||||
$scope.time = getScopeTimeObj(time.from, time.to);
|
||||
|
||||
$scope.onAppEvent('zoom-out', function() {
|
||||
$scope.zoom(2);
|
||||
});
|
||||
};
|
||||
|
||||
$scope.customTime = function() {
|
||||
// Assume the form is valid since we're setting it to something valid
|
||||
$scope.input.$setValidity("dummy", true);
|
||||
$scope.temptime = cloneTime($scope.time);
|
||||
$scope.temptime.now = $scope.panel.now;
|
||||
|
||||
$scope.temptime.from.date.setHours(0,0,0,0);
|
||||
$scope.temptime.to.date.setHours(0,0,0,0);
|
||||
|
||||
// Date picker needs the date to be at the start of the day
|
||||
if(new Date().getTimezoneOffset() < 0) {
|
||||
$scope.temptime.from.date = moment($scope.temptime.from.date).add(1, 'days').toDate();
|
||||
$scope.temptime.to.date = moment($scope.temptime.to.date).add(1, 'days').toDate();
|
||||
}
|
||||
|
||||
$scope.appEvent('show-dash-editor', {src: 'app/panels/timepicker/custom.html', scope: $scope });
|
||||
};
|
||||
|
||||
// Constantly validate the input of the fields. This function does not change any date variables
|
||||
// outside of its own scope
|
||||
$scope.validate = function(time) {
|
||||
// Assume the form is valid. There is a hidden dummy input for invalidating it programatically.
|
||||
$scope.input.$setValidity("dummy", true);
|
||||
|
||||
var _from = datepickerToLocal(time.from.date),
|
||||
_to = datepickerToLocal(time.to.date),
|
||||
_t = time;
|
||||
|
||||
if($scope.input.$valid) {
|
||||
|
||||
_from.setHours(_t.from.hour,_t.from.minute,_t.from.second,_t.from.millisecond);
|
||||
_to.setHours(_t.to.hour,_t.to.minute,_t.to.second,_t.to.millisecond);
|
||||
|
||||
// Check that the objects are valid and to is after from
|
||||
if(isNaN(_from.getTime()) || isNaN(_to.getTime()) || _from.getTime() >= _to.getTime()) {
|
||||
$scope.input.$setValidity("dummy", false);
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
|
||||
return { from: _from, to:_to, now: time.now};
|
||||
};
|
||||
|
||||
$scope.setNow = function() {
|
||||
$scope.time.to = getTimeObj(new Date());
|
||||
};
|
||||
|
||||
/*
|
||||
time : {
|
||||
from: Date
|
||||
to: Date
|
||||
}
|
||||
*/
|
||||
$scope.setAbsoluteTimeFilter = function (time) {
|
||||
// Create filter object
|
||||
var _filter = _.clone(time);
|
||||
|
||||
if(time.now) {
|
||||
_filter.to = "now";
|
||||
}
|
||||
|
||||
// Set the filter
|
||||
$scope.panel.filter_id = timeSrv.setTime(_filter);
|
||||
|
||||
// Update our representation
|
||||
$scope.time = getScopeTimeObj(time.from,time.to);
|
||||
};
|
||||
|
||||
$scope.setRelativeFilter = function(timespan) {
|
||||
$scope.panel.now = true;
|
||||
|
||||
var _filter = {
|
||||
from : "now-"+timespan,
|
||||
to: "now"
|
||||
};
|
||||
|
||||
if ($scope.panel.nowDelay) {
|
||||
_filter.to = 'now-' + $scope.panel.nowDelay;
|
||||
}
|
||||
|
||||
timeSrv.setTime(_filter);
|
||||
|
||||
$scope.time = getScopeTimeObj(kbn.parseDate(_filter.from),new Date());
|
||||
};
|
||||
|
||||
var pad = function(n, width, z) {
|
||||
z = z || '0';
|
||||
n = n.toString();
|
||||
return n.length >= width ? n : new Array(width - n.length + 1).join(z) + n;
|
||||
};
|
||||
|
||||
var cloneTime = function(time) {
|
||||
var _n = {
|
||||
from: _.clone(time.from),
|
||||
to: _.clone(time.to)
|
||||
};
|
||||
// Create new dates as _.clone is shallow.
|
||||
_n.from.date = new Date(_n.from.date);
|
||||
_n.to.date = new Date(_n.to.date);
|
||||
return _n;
|
||||
};
|
||||
|
||||
var getScopeTimeObj = function(from,to) {
|
||||
var model = { from: getTimeObj(from), to: getTimeObj(to), };
|
||||
|
||||
if (model.from.date) {
|
||||
model.tooltip = $scope.dashboard.formatDate(model.from.date) + ' <br>to<br>';
|
||||
model.tooltip += $scope.dashboard.formatDate(model.to.date);
|
||||
}
|
||||
else {
|
||||
model.tooltip = 'Click to set time filter';
|
||||
}
|
||||
|
||||
if (timeSrv.time) {
|
||||
if ($scope.panel.now) {
|
||||
model.rangeString = moment(model.from.date).fromNow() + ' to ' +
|
||||
moment(model.to.date).fromNow();
|
||||
}
|
||||
else {
|
||||
model.rangeString = $scope.dashboard.formatDate(model.from.date, 'MMM D, YYYY HH:mm:ss') + ' to ' +
|
||||
$scope.dashboard.formatDate(model.to.date, 'MMM D, YYYY HH:mm:ss');
|
||||
}
|
||||
}
|
||||
|
||||
return model;
|
||||
};
|
||||
|
||||
var getTimeObj = function(date) {
|
||||
return {
|
||||
date: new Date(date),
|
||||
hour: pad(date.getHours(),2),
|
||||
minute: pad(date.getMinutes(),2),
|
||||
second: pad(date.getSeconds(),2),
|
||||
millisecond: pad(date.getMilliseconds(),3)
|
||||
};
|
||||
};
|
||||
|
||||
// Do not use the results of this function unless you plan to use setHour/Minutes/etc on the result
|
||||
var datepickerToLocal = function(date) {
|
||||
date = moment(date).clone().toDate();
|
||||
return moment(new Date(date.getTime() + date.getTimezoneOffset() * 60000)).toDate();
|
||||
};
|
||||
|
||||
$scope.zoom = function(factor) {
|
||||
var range = timeSrv.timeRange();
|
||||
|
||||
var timespan = (range.to.valueOf() - range.from.valueOf());
|
||||
var center = range.to.valueOf() - timespan/2;
|
||||
|
||||
var to = (center + (timespan*factor)/2);
|
||||
var from = (center - (timespan*factor)/2);
|
||||
|
||||
if(to > Date.now() && range.to <= Date.now()) {
|
||||
var offset = to - Date.now();
|
||||
from = from - offset;
|
||||
to = Date.now();
|
||||
}
|
||||
|
||||
timeSrv.setTime({
|
||||
from: moment.utc(from).toDate(),
|
||||
to: moment.utc(to).toDate(),
|
||||
});
|
||||
};
|
||||
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user