Merge branch 'master' into panel_edit_menu_poc

This commit is contained in:
Torkel Ödegaard 2014-09-20 13:37:02 +02:00
commit 6003fee33f
61 changed files with 1019 additions and 254 deletions

View File

@ -1,5 +1,14 @@
# 1.8.0 (unreleased)
**Fixes**
- [Issue #802](https://github.com/grafana/grafana/issues/802). Annotations: Fix when using InfluxDB datasource
- [Issue #795](https://github.com/grafana/grafana/issues/795). Chrome: Fix for display issue in chrome beta & chrome canary when entering edit mode
- [Issue #818](https://github.com/grafana/grafana/issues/818). Graph: Added percent y-axis format
- [Issue #828](https://github.com/grafana/grafana/issues/828). Elasticsearch: saving new dashboard with title equal to slugified url would cause it to deleted.
- [Issue #830](https://github.com/grafana/grafana/issues/830). Annotations: Fix for elasticsearch annotations and mapping nested fields
# 1.8.0-RC1 (2014-09-12)
**UI polish / changes**
- [Issue #725](https://github.com/grafana/grafana/issues/725). UI: All modal editors are removed and replaced by an edit pane under menu. The look of editors is also updated and polished. Search dropdown is also shown as pane under menu and has seen some UI polish.
@ -13,6 +22,8 @@
- [Issue #262](https://github.com/grafana/grafana/issues/262). Templating: Ability to use template variables for function parameters via custom variable type, can be used as parameter for movingAverage or scaleToSeconds for example
- [Issue #312](https://github.com/grafana/grafana/issues/312). Templating: Can now use template variables in panel titles
- [Issue #613](https://github.com/grafana/grafana/issues/613). Templating: Full support for InfluxDB, filter by part of series names, extract series substrings, nested queries, multipe where clauses!
- Template variables can be initialized from url, with var-my_varname=value, breaking change, before it was just my_varname.
- Templating and url state sync has some issues that are not solved for this release, see [Issue #772](https://github.com/grafana/grafana/issues/772) for more details.
**InfluxDB Breaking changes**
- To better support templating, fill(0) and group by time low limit some changes has been made to the editor and query model schema
@ -41,6 +52,9 @@
- [Issue #425](https://github.com/grafana/grafana/issues/425). Graph: New section in 'Display Styles' tab to override any display setting on per series bases (mix and match lines, bars, points, fill, stack, line width etc)
- [Issue #634](https://github.com/grafana/grafana/issues/634). Dashboard: Dashboard tags now in different colors (from fixed palette) determined by tag name.
- [Issue #685](https://github.com/grafana/grafana/issues/685). Dashboard: New config.js option to change/remove window title prefix.
- [Issue #781](https://github.com/grafana/grafana/issues/781). Dashboard: Title URL is now slugified for greater URL readability, works with both ES & InfluxDB storage, is backward compatible
- [Issue #785](https://github.com/grafana/grafana/issues/785). Elasticsearch: Support for full elasticsearch lucene search grammar when searching for dashboards, better async search
- [Issue #787](https://github.com/grafana/grafana/issues/787). Dashboard: time range can now be read from URL parameters, will override dashboard saved time range
**Fixes**
- [Issue #696](https://github.com/grafana/grafana/issues/696). Graph: Fix for y-axis format 'none' when values are in scientific notation (ex 2.3e-13)
@ -234,7 +248,7 @@ Read this for more info:
- More graphite function definitions
- Make "ms" axis format include hour, day, weeks, month and year ([Issue #149](https://github.com/grafana/grafana/issues/149))
- Microsecond axis format ([Issue #146](https://github.com/grafana/grafana/issues/146))
- Specify template paramaters in URL ([Issue #123](https://github.com/grafana/grafana/issues/123))
- Specify template parameters in URL ([Issue #123](https://github.com/grafana/grafana/issues/123))
### Fixes
- Basic Auth fix ([Issue #152](https://github.com/grafana/grafana/issues/152))

View File

@ -1,4 +1,4 @@
{
"version": "1.7.0",
"url": "http://grafanarel.s3.amazonaws.com/grafana-1.7.0"
"version": "1.8.0-rc1",
"url": "http://grafanarel.s3.amazonaws.com/grafana-1.8.0-rc1"
}

View File

@ -4,7 +4,7 @@
"company": "Coding Instinct AB"
},
"name": "grafana",
"version": "1.8.0",
"version": "1.8.0-rc1",
"repository": {
"type": "git",
"url": "http://github.com/torkelo/grafana.git"

View File

@ -531,6 +531,10 @@ function($, _, moment) {
return function(val) {
return kbn.nanosFormat(val, decimals);
};
case 'percent':
return function(val, axis) {
return kbn.noneFormat(val, axis ? axis.tickDecimals : null) + ' %';
};
default:
return function(val, axis) {
return kbn.noneFormat(val, axis ? axis.tickDecimals : null);
@ -563,8 +567,8 @@ function($, _, moment) {
kbn.msFormat = function(size, decimals) {
// Less than 1 milli, downscale to micro
if (Math.abs(size) < 1) {
return kbn.microsFormat(size * 1000,decimals);
if (size !== 0 && Math.abs(size) < 1) {
return kbn.microsFormat(size * 1000, decimals);
}
else if (Math.abs(size) < 1000) {
return size.toFixed(decimals) + " ms";
@ -591,7 +595,7 @@ function($, _, moment) {
kbn.sFormat = function(size, decimals) {
// Less than 1 sec, downscale to milli
if (Math.abs(size) < 1) {
if (size !== 0 && Math.abs(size) < 1) {
return kbn.msFormat(size * 1000, decimals);
}
// Less than 10 min, use seconds
@ -620,7 +624,7 @@ function($, _, moment) {
kbn.microsFormat = function(size, decimals) {
// Less than 1 micro, downscale to nano
if (Math.abs(size) < 1) {
if (size !== 0 && Math.abs(size) < 1) {
return kbn.nanosFormat(size * 1000, decimals);
}
else if (Math.abs(size) < 1000) {
@ -655,6 +659,13 @@ function($, _, moment) {
}
};
kbn.slugifyForUrl = function(str) {
return str
.toLowerCase()
.replace(/[^\w ]+/g,'')
.replace(/ +/g,'-');
};
kbn.stringToJsRegex = function(str) {
if (str[0] !== '/') {
return new RegExp(str);

View File

@ -44,14 +44,14 @@ function (angular, $, config, _) {
$scope.setupDashboard = function(event, dashboardData) {
$rootScope.performance.dashboardLoadStart = new Date().getTime();
$rootScope.performance.panelsInitialized = 0;
$rootScope.performance.panelsRendered= 0;
$rootScope.performance.panelsRendered = 0;
$scope.dashboard = dashboardSrv.create(dashboardData);
$scope.dashboardViewState = dashboardViewStateSrv.create($scope);
// init services
timeSrv.init($scope.dashboard);
templateValuesSrv.init($scope.dashboard);
templateValuesSrv.init($scope.dashboard, $scope.dashboardViewState);
panelMoveSrv.init($scope.dashboard, $scope);
$scope.checkFeatureToggles();
@ -93,18 +93,18 @@ function (angular, $, config, _) {
};
};
$scope.panel_path =function(type) {
if(type) {
return 'app/panels/'+type.replace(".","/");
$scope.edit_path = function(type) {
var p = $scope.panel_path(type);
if(p) {
return p+'/editor.html';
} else {
return false;
}
};
$scope.edit_path = function(type) {
var p = $scope.panel_path(type);
if(p) {
return p+'/editor.html';
$scope.panel_path =function(type) {
if(type) {
return 'app/panels/'+type.replace(".","/");
} else {
return false;
}

View File

@ -78,7 +78,7 @@ function (angular, _, moment, config, store) {
var clone = angular.copy($scope.dashboard);
$scope.db.saveDashboard(clone)
.then(function(result) {
alertSrv.set('Dashboard Saved', 'Dashboard has been saved as "' + result.title + '"','success', 5000);
alertSrv.set('Dashboard Saved', 'Saved as "' + result.title + '"','success', 3000);
if (result.url !== $location.path()) {
$location.search({});
@ -88,7 +88,7 @@ function (angular, _, moment, config, store) {
$rootScope.$emit('dashboard-saved', $scope.dashboard);
}, function(err) {
alertSrv.set('Save failed', err, 'error',5000);
alertSrv.set('Save failed', err, 'error', 5000);
});
};

View File

@ -15,7 +15,7 @@ function (angular, _, config, gfunc, Parser) {
$scope.init = function() {
$scope.target.target = $scope.target.target || '';
$scope.targetLetter = targetLetters[$scope.$index];
$scope.targetLetters = targetLetters;
parseTarget();
};
@ -90,7 +90,7 @@ function (angular, _, config, gfunc, Parser) {
break;
case 'metric':
if ($scope.segments.length > 0) {
if ($scope.segments[0].length !== 1) {
if (astNode.segments.length !== 1) {
throw { message: 'Multiple metric params not supported, use text editor.' };
}
addFunctionParameter(func, astNode.segments[0].value, index, true);

View File

@ -17,6 +17,7 @@ function (angular, _, config, $) {
$scope.results = {dashboards: [], tags: [], metrics: []};
$scope.query = { query: 'title:' };
$scope.db = datasourceSrv.getGrafanaDB();
$scope.currentSearchId = 0;
$timeout(function() {
$scope.giveSearchFocus = $scope.giveSearchFocus + 1;
@ -75,8 +76,18 @@ function (angular, _, config, $) {
};
$scope.searchDashboards = function(queryString) {
// bookeeping for determining stale search requests
var searchId = $scope.currentSearchId + 1;
$scope.currentSearchId = searchId > $scope.currentSearchId ? searchId : $scope.currentSearchId;
return $scope.db.searchDashboards(queryString)
.then(function(results) {
// since searches are async, it's possible that these results are not for the latest search. throw
// them away if so
if (searchId < $scope.currentSearchId) {
return;
}
$scope.tagsOnly = results.tagsOnly;
$scope.results.dashboards = results.dashboards;
$scope.results.tags = results.tags;
@ -108,9 +119,10 @@ function (angular, _, config, $) {
$scope.searchDashboards($scope.query.query);
};
$scope.deleteDashboard = function(id, evt) {
$scope.deleteDashboard = function(dash, evt) {
evt.stopPropagation();
$scope.emitAppEvent('delete-dashboard', { id: id });
$scope.emitAppEvent('delete-dashboard', { id: dash.id });
$scope.results.dashboards = _.without($scope.results.dashboards, dash);
};
$scope.addMetricToCurrentDashboard = function (metricId) {

View File

@ -9,14 +9,15 @@
"title": "New row",
"height": "150px",
"collapse": false,
"editable": true,
"panels": [
{
"error": false,
"id": 1,
"span": 12,
"editable": true,
"type": "text",
"mode": "html",
"content": "<div class=\"text-center\" style=\"padding-top: 15px\">\n<img src=\"http://grafana.org/assets/img/logo_transparent_200x75.png\"> \n</div>",
"content": "<div class=\"text-center\" style=\"padding-top: 15px\">\n<img src=\"//grafana.org/assets/img/logo_transparent_200x75.png\"> \n</div>",
"style": {},
"title": "Welcome to"
}
@ -26,22 +27,20 @@
"title": "Welcome to Grafana",
"height": "210px",
"collapse": false,
"editable": true,
"panels": [
{
"error": false,
"id": 2,
"span": 6,
"editable": true,
"type": "text",
"loadingEditor": false,
"mode": "html",
"content": "<br/>\n\n<div class=\"row-fluid\">\n <div class=\"span6\">\n <ul>\n <li>\n <a href=\"http://grafana.org/docs#configuration\" target=\"_blank\">Configuration</a>\n </li>\n <li>\n <a href=\"http://grafana.org/docs/troubleshooting\" target=\"_blank\">Troubleshooting</a>\n </li>\n <li>\n <a href=\"http://grafana.org/docs/support\" target=\"_blank\">Support</a>\n </li>\n <li>\n <a href=\"http://grafana.org/docs/features/intro\" target=\"_blank\">Getting started</a> (Must read!)\n </li>\n </ul>\n </div>\n <div class=\"span6\">\n <ul>\n <li>\n <a href=\"http://grafana.org/docs/features/graphing\" target=\"_blank\">Graphing</a>\n </li>\n <li>\n <a href=\"http://grafana.org/docs/features/annotations\" target=\"_blank\">Annotations</a>\n </li>\n <li>\n <a href=\"http://grafana.org/docs/features/graphite\" target=\"_blank\">Graphite</a>\n </li>\n <li>\n <a href=\"http://grafana.org/docs/features/influxdb\" target=\"_blank\">InfluxDB</a>\n </li>\n <li>\n <a href=\"http://grafana.org/docs/features/opentsdb\" target=\"_blank\">OpenTSDB</a>\n </li>\n </ul>\n </div>\n</div>",
"style": {},
"title": "Documentation Links"
},
{
"error": false,
"id": 3,
"span": 6,
"editable": true,
"type": "text",
"mode": "html",
"content": "<br/>\n\n<div class=\"row-fluid\">\n <div class=\"span12\">\n <ul>\n <li>Ctrl+S saves the current dashboard</li>\n <li>Ctrl+F Opens the dashboard finder</li>\n <li>Ctrl+H Hide/show row controls</li>\n <li>Click and drag graph title to move panel</li>\n <li>Hit Escape to exit graph when in fullscreen or edit mode</li>\n <li>Click the colored icon in the legend to change series color</li>\n <li>Ctrl or Shift + Click legend name to hide other series</li>\n </ul>\n </div>\n</div>\n",
@ -53,11 +52,12 @@
{
"title": "test",
"height": "250px",
"editable": true,
"collapse": false,
"panels": [
{
"id": 4,
"span": 12,
"editable": true,
"type": "graph",
"x-axis": true,
"y-axis": true,

View File

@ -35,11 +35,9 @@ return function(callback) {
// Set a title
dashboard.title = 'Scripted dash';
dashboard.services.filter = {
time: {
from: "now-" + (ARGS.from || timspan),
to: "now"
}
dashboard.time = {
from: "now-" + (ARGS.from || timspan),
to: "now"
};
var rows = 1;
@ -78,4 +76,4 @@ return function(callback) {
callback(dashboard);
});
}
}

View File

@ -0,0 +1,264 @@
{
"id": null,
"title": "Templated Graphs Nested",
"originalTitle": "Templated Graphs Nested",
"tags": [
"showcase",
"templated"
],
"style": "dark",
"timezone": "browser",
"editable": true,
"hideControls": false,
"rows": [
{
"title": "Row1",
"height": "350px",
"editable": true,
"collapse": false,
"collapsable": true,
"panels": [
{
"span": 12,
"editable": true,
"type": "graph",
"loadingEditor": false,
"datasource": null,
"renderer": "flot",
"x-axis": true,
"y-axis": true,
"scale": 1,
"y_formats": [
"short",
"short"
],
"grid": {
"max": null,
"min": 0,
"threshold1": null,
"threshold2": null,
"threshold1Color": "rgba(216, 200, 27, 0.27)",
"threshold2Color": "rgba(234, 112, 112, 0.22)",
"leftMax": null,
"rightMax": null,
"leftMin": null,
"rightMin": null
},
"annotate": {
"enable": false
},
"resolution": 100,
"lines": true,
"fill": 1,
"linewidth": 1,
"points": false,
"pointradius": 5,
"bars": false,
"stack": true,
"legend": {
"show": true,
"values": false,
"min": false,
"max": false,
"current": false,
"total": false,
"avg": false
},
"percentage": false,
"zerofill": true,
"nullPointMode": "connected",
"steppedLine": false,
"tooltip": {
"value_type": "cumulative",
"query_as_alias": true
},
"targets": [
{
"target": "aliasByNode(apps.$app.$server.counters.requests.count, 2)",
"function": "mean",
"column": "value"
}
],
"aliasColors": {
"highres.test": "#1F78C1",
"scale(highres.test,3)": "#6ED0E0",
"mobile": "#6ED0E0",
"tablet": "#EAB839"
},
"title": "Traffic [[period]]",
"id": 1,
"seriesOverrides": []
}
],
"notice": false
},
{
"title": "Row1",
"height": "350px",
"editable": true,
"collapse": false,
"collapsable": true,
"panels": [
{
"span": 12,
"editable": true,
"type": "graph",
"loadingEditor": false,
"datasource": null,
"renderer": "flot",
"x-axis": true,
"y-axis": true,
"scale": 1,
"y_formats": [
"short",
"short"
],
"grid": {
"max": null,
"min": 0,
"threshold1": null,
"threshold2": null,
"threshold1Color": "rgba(216, 200, 27, 0.27)",
"threshold2Color": "rgba(234, 112, 112, 0.22)",
"leftMax": null,
"rightMax": null,
"leftMin": null,
"rightMin": null
},
"annotate": {
"enable": false
},
"resolution": 100,
"lines": true,
"fill": 1,
"linewidth": 1,
"points": false,
"pointradius": 5,
"bars": false,
"stack": true,
"legend": {
"show": true,
"values": false,
"min": false,
"max": false,
"current": false,
"total": false,
"avg": false
},
"percentage": false,
"zerofill": true,
"nullPointMode": "connected",
"steppedLine": false,
"tooltip": {
"value_type": "cumulative",
"query_as_alias": true
},
"targets": [
{
"target": "aliasByNode(apps.$app.$server.counters.requests.count, 2)"
}
],
"aliasColors": {
"highres.test": "#1F78C1",
"scale(highres.test,3)": "#6ED0E0",
"mobile": "#6ED0E0",
"tablet": "#EAB839"
},
"title": "Second pannel",
"id": 2,
"seriesOverrides": []
}
],
"notice": false
}
],
"nav": [
{
"type": "timepicker",
"collapse": false,
"notice": false,
"enable": true,
"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"
],
"now": true
}
],
"time": {
"from": "now-15m",
"to": "now"
},
"templating": {
"list": [
{
"type": "query",
"name": "app",
"query": "apps.*",
"includeAll": true,
"options": [],
"current": {
"text": "All",
"value": "*"
},
"datasource": null,
"allFormat": "wildcard",
"refresh": true
},
{
"type": "query",
"name": "server",
"query": "apps.$app.*",
"includeAll": true,
"options": [],
"current": {
"text": "All",
"value": "*"
},
"datasource": null,
"allFormat": "Glob",
"refresh": false
},
{
"type": "query",
"datasource": null,
"refresh_on_load": false,
"name": "metric",
"options": [],
"includeAll": true,
"allFormat": "glob",
"query": "apps.$app.$server.*",
"current": {
"text": "counters",
"value": "counters"
}
}
],
"enable": true
},
"annotations": {
"enable": false
},
"refresh": false,
"version": 6
}

View File

@ -15,7 +15,7 @@ function (angular, $, kbn, moment, _) {
restrict: 'A',
template: '<div> </div>',
link: function(scope, elem) {
var data, plot, annotations;
var data, annotations;
var hiddenData = {};
var dashboard = scope.dashboard;
var legendSideLastValue = null;
@ -82,6 +82,10 @@ function (angular, $, kbn, moment, _) {
render_panel_as_graphite_png(data);
return true;
}
if (elem.width() === 0) {
return;
}
}
// Function for rendering panel
@ -165,18 +169,22 @@ function (angular, $, kbn, moment, _) {
var sortedSeries = _.sortBy(data, function(series) { return series.zindex; });
// if legend is to the right delay plot draw a few milliseconds
// so the legend width calculation can be done
function callPlot() {
try {
$.plot(elem, sortedSeries, options);
} catch (e) {
console.log('flotcharts error', e);
}
addAxisLabels();
}
if (shouldDelayDraw(panel)) {
setTimeout(callPlot, 50);
legendSideLastValue = panel.legend.rightSide;
setTimeout(function() {
plot = $.plot(elem, sortedSeries, options);
addAxisLabels();
}, 50);
}
else {
plot = $.plot(elem, sortedSeries, options);
addAxisLabels();
callPlot();
}
}

View File

@ -94,7 +94,7 @@ function (angular, app, _, $) {
};
$input.attr('data-provide', 'typeahead');
$input.typeahead({ source: $scope.source, minLength: 0, items: 100, updater: $scope.updater });
$input.typeahead({ source: $scope.source, minLength: 0, items: 10000, updater: $scope.updater });
var typeahead = $input.data('typeahead');
typeahead.lookup = function () {

View File

@ -11,10 +11,10 @@ function (angular, kbn) {
return {
restrict: 'E',
link: function(scope, elem, attrs) {
var _t = '<i class="icon-'+(attrs.icon||'question-sign')+'" bs-tooltip="\''+
var _t = '<i class="grafana-tip icon-'+(attrs.icon||'question-sign')+'" bs-tooltip="\''+
kbn.addslashes(elem.text())+'\'"></i>';
elem.replaceWith($compile(angular.element(_t))(scope));
}
};
});
});
});

View File

@ -57,7 +57,7 @@ define(['angular', 'jquery', 'lodash', 'moment'], function (angular, $, _, momen
module.filter('interpolateTemplateVars', function(templateSrv) {
return function(text) {
return templateSrv.replace(text);
return templateSrv.replaceWithText(text);
};
});

View File

@ -4,7 +4,7 @@
<h5>Left Y Axis</h5>
<div class="editor-option">
<label class="small">Format <tip>Y-axis formatting</tip></label>
<select class="input-small" ng-model="panel.y_formats[0]" ng-options="f for f in ['none','short','bytes', 'bits', 'bps', 's', 'ms', 'µs', 'ns']" ng-change="render()"></select>
<select class="input-small" ng-model="panel.y_formats[0]" ng-options="f for f in ['none','short','bytes', 'bits', 'bps', 's', 'ms', 'µs', 'ns', 'percent']" ng-change="render()"></select>
</div>
<div class="editor-option">
<label class="small">Min / <a ng-click="toggleGridMinMax('leftMin')">Auto <i class="icon-star" ng-show="_.isNull(panel.grid.leftMin)"></i></a></label>
@ -23,7 +23,7 @@
<h5>Right Y Axis</h5>
<div class="editor-option">
<label class="small">Format <tip>Y-axis formatting</tip></label>
<select class="input-small" ng-model="panel.y_formats[1]" ng-options="f for f in ['none','short','bytes', 'bits', 'bps', 's', 'ms', 'µs', 'ns']" ng-change="render()"></select>
<select class="input-small" ng-model="panel.y_formats[1]" ng-options="f for f in ['none','short','bytes', 'bits', 'bps', 's', 'ms', 'µs', 'ns', 'percent']" ng-change="render()"></select>
</div>
<div class="editor-option">
<label class="small">Min / <a ng-click="toggleGridMinMax('rightMin')">Auto <i class="icon-star" ng-show="_.isNull(panel.grid.rightMin)"></i></a></label>

View File

@ -70,7 +70,7 @@ define([
$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]);
$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();

View File

@ -27,7 +27,7 @@
<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>
<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>
<div class="editor-option">

View File

@ -9,10 +9,9 @@
</div>
<label class=small>Content
<span ng-show="panel.mode == 'html'">(This area uses HTML sanitized via AngularJS's <a href='http://docs.angularjs.org/api/ngSanitize.$sanitize'>$sanitize</a> service)</span>
<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="6" style="width:95%" ng-change="render()" ng-model-onblur>
<textarea ng-model="panel.content" rows="20" style="width:95%" ng-change="render()" ng-model-onblur>
</textarea>
</div>
</div>

View File

@ -16,6 +16,9 @@
<div class="dashboard-editor-body">
<div class="editor-row row" ng-if="editor.index == 0">
<div class="span6">
<div ng-if="variables.length === 0">
<em>No annotations defined</em>
</div>
<table class="grafana-options-table">
<tr ng-repeat="annotation in annotations">
<td style="width:90%">

View File

@ -1,7 +1,10 @@
<div class="navbar navbar-static-top">
<div class="navbar-inner">
<div class="container-fluid">
<span class="brand"><img src="img/small.png" bs-tooltip="'Grafana'" data-placement="bottom"> {{dashboard.title}}</span>
<span class="brand">
<img class="logo-icon" src="img/fav32.png" bs-tooltip="'Grafana'" data-placement="bottom"></img>
<span class="page-title">{{dashboard.title}}</span>
</span>
<ul class="nav pull-right" ng-controller='DashboardNavCtrl' ng-init="init()">
<li ng-show="dashboardViewState.fullscreen">

View File

@ -8,7 +8,7 @@
</div>
<div class="dashboard-editor-body" style="height: 500px">
<textarea ng-model="json" rows="20" spellcheck="false" style="width: 90%; color: white"></textarea>
<textarea ng-model="json" rows="20" spellcheck="false" style="width: 90%;"></textarea>
</div>
<div class="dashboard-editor-footer">

View File

@ -1,4 +1,4 @@
<div class="editor-row" style="margin-top: 10px;">
<div class="editor-row">
<div ng-repeat="target in panel.targets"
class="grafana-target"
@ -30,13 +30,6 @@
ng-click="duplicate()">
Duplicate
</a>
</li>
<li role="menuitem">
<a tabindex="1"
ng-click="toggleMetricOptions()">
Toggle request options
</a>
</li>
</ul>
</li>
<li>
@ -48,7 +41,7 @@
<ul class="grafana-segment-list">
<li class="grafana-target-segment" style="min-width: 15px; text-align: center">
{{targetLetter}}
{{targetLetters[$index]}}
</li>
<li>
<a class="grafana-target-segment"
@ -80,24 +73,109 @@
</div>
</div>
<div class="grafana-target grafana-metric-options" ng-if="panel.metricOptionsEnabled">
</div>
<section class="grafana-metric-options">
<div class="grafana-target-inner">
<ul class="grafana-segment-list">
<li class="grafana-target-segment grafana-target-segment-icon">
<i class="icon-wrench"></i>
</li>
<li class="grafana-target-segment">
cacheTimeout <tip>Graphite parameter to overwride memcache default timeout (unit is seconds)</tip>
cacheTimeout
</li>
<li>
<input type="text"
class="input-large grafana-target-segment-input"
ng-model="panel.cacheTimeout"
spellcheck='false'
placeholder="60">
class="input-mini grafana-target-segment-input"
ng-model="panel.cacheTimeout"
bs-tooltip="'Graphite parameter to overwride memcache default timeout (unit is seconds)'"
data-placement="right"
spellcheck='false'
placeholder="60">
</li>
</ul>
<div class="clearfix"></div>
<div class="clearfix"></div>
</div>
<div class="grafana-target-inner">
<ul class="grafana-segment-list">
<li class="grafana-target-segment grafana-target-segment-icon">
<i class="icon-info-sign"></i>
</li>
<li class="grafana-target-segment">
<a ng-click="toggleEditorHelp(1);" bs-tooltip="'click to show helpful info'" data-placement="bottom">
shorter legend names
</a>
</li>
<li class="grafana-target-segment">
<a ng-click="toggleEditorHelp(2);" bs-tooltip="'click to show helpful info'" data-placement="bottom">
series as parameters
</a>
</li>
<li class="grafana-target-segment">
<a ng-click="toggleEditorHelp(3)" bs-tooltip="'click to show helpful info'" data-placement="bottom">
stacking
</a>
</li>
<li class="grafana-target-segment">
<a ng-click="toggleEditorHelp(4)" bs-tooltip="'click to show helpful info'" data-placement="bottom">
templating
</a>
</li>
</ul>
<div class="clearfix"></div>
</div>
</div>
</section>
<div class="editor-row">
<div class="pull-left" style="margin-top: 30px;">
<div class="grafana-info-box span8" ng-if="editorHelpIndex === 1">
<h5>Shorter legend names</h5>
<ul>
<li>alias() function to specify a custom series name</li>
<li>aliasByNode(2) to alias by a specific part of your metric path</li>
<li>aliasByNode(2, -1) you can add multiple segment paths, and use negative index</li>
<li>groupByNode(2, 'sum') is useful if you have 2 wildcards in your metric path and want to sumSeries and group by</li>
</ul>
</div>
<div class="grafana-info-box span8" ng-if="editorHelpIndex === 2">
<h5>Series as parameter</h5>
<ul>
<li>Some graphite functions allow you to have many series arguments</li>
<li>Use #[A-Z] to use a graphite query as parameter to a function</li>
<li>
Examples:
<ul>
<li>asPercent(#A, #B)</li>
<li>prod.srv-01.counters.count - asPercent(#A) : percentage of count in comparison with A query</li>
<li>prod.srv-01.counters.count - sumSeries(#A) : sum count and series A </li>
<li>divideSeries(#A, #B)</li>
</ul>
</li>
<li>If a query is added only to be used as a parameter, hide it from the graph with the eye icon</li>
</ul>
</div>
<div class="grafana-info-box span6" ng-if="editorHelpIndex === 3">
<h5>Stacking</h5>
<ul>
<li>You find the stacking option under Display Styles tab</li>
<li>When stacking is enabled make sure null point mode is set to 'null as zero'</li>
</ul>
</div>
<div class="grafana-info-box span6" ng-if="editorHelpIndex === 4">
<h5>Templating</h5>
<ul>
<li>You can use a template variable in place of metric names</li>
<li>You can use a template variable in place of function parameters</li>
<li>You enable the templating feature in Dashboard settings / Feature toggles </li>
</ul>
</div>
</div>
</div>

View File

@ -181,8 +181,8 @@
<div class="grafana-target">
<div class="grafana-target-inner">
<ul class="grafana-segment-list">
<li class="grafana-target-segment">
<i class="icon-cogs"></i>
<li class="grafana-target-segment grafana-target-segment-icon">
<i class="icon-wrench"></i>
</li>
<li class="grafana-target-segment">
group by time
@ -227,8 +227,8 @@
<div class="editor-row">
<div class="pull-left" style="margin-top: 30px;">
<div class="span6" ng-if="editorHelpIndex === 1">
Alias patterns:
<div class="grafana-info-box span6" ng-if="editorHelpIndex === 1">
<h5>Alias patterns</h5>
<ul>
<li>$s = series name</li>
<li>$g = group by</li>
@ -236,8 +236,8 @@
</ul>
</div>
<div class="span6" ng-if="editorHelpIndex === 2">
Stacking and fill:
<div class="grafana-info-box span6" ng-if="editorHelpIndex === 2">
<h5>Stacking and fill</h5>
<ul>
<li>When stacking is enabled it important that points align</li>
<li>If there are missing points for one series it can cause gaps or missing bars</li>
@ -247,8 +247,8 @@
</ul>
</div>
<div class="span6" ng-if="editorHelpIndex === 3">
Group by time:
<div class="grafana-info-box span6" ng-if="editorHelpIndex === 3">
<h5>Group by time</h5>
<ul>
<li>Group by time is important, otherwise the query could return many thousands of datapoints that will slow down Grafana</li>
<li>Leave the group by time field empty for each query and it will be calculated based on time range and pixel width of the graph</li>

View File

@ -30,7 +30,7 @@
</li>
</ul>
<ul class="grafana-target-segment-list">
<ul class="grafana-segment-list">
<li>
<a class="grafana-target-segment"
ng-click="target.hide = !target.hide; get_data();"

View File

@ -1,6 +0,0 @@
<div ng-include="'app/partials/panelgeneral.html'"></div>
<div ng-if="!panelMeta.fullEditorTabs" ng-include="edit_path(panel.type)"></div>
<div ng-repeat="tab in panelMeta.editorTabs">
<h5>{{tab.title}}</h5>
<div ng-include="tab.src"></div>
</div>

View File

@ -38,7 +38,7 @@
<div class="editor-option">
<div class="span4">
<span><i class="icon-question-sign"></i>
dashboards available in the playlist are only the once marked as favorites (stored in local browser storage).
dashboards available in the playlist are only the ones marked as favorites (stored in local browser storage).
to mark a dashboard as favorite, use save icon in the menu and in the dropdown select mark as favorite
<br/><br/>
</span>

View File

@ -54,7 +54,7 @@
<a ng-click="shareDashboard(row.id, row.id, $event)" config-modal="app/partials/dashLoaderShare.html">
<i class="icon-share"></i> share &nbsp;&nbsp;&nbsp;
</a>
<a ng-click="deleteDashboard(row.id, $event)">
<a ng-click="deleteDashboard(row, $event)">
<i class="icon-remove"></i> delete
</a>
</div>

View File

@ -57,7 +57,8 @@ define([
function errorHandler(err) {
console.log('Annotation error: ', err);
alertSrv.set('Annotations','Could not fetch annotations','error');
var message = err.message || "Aannotation query failed";
alertSrv.set('Annotations error', message,'error');
}
function addAnnotation(options) {

View File

@ -18,8 +18,8 @@ function(angular, $) {
keyboardManager.unbind('ctrl+s');
keyboardManager.unbind('ctrl+r');
keyboardManager.unbind('ctrl+z');
keyboardManager.unbind('esc');
});
keyboardManager.unbind('esc');
keyboardManager.bind('ctrl+f', function() {
scope.emitAppEvent('show-dash-editor', { src: 'app/partials/search.html' });

View File

@ -131,7 +131,7 @@ function (angular, $, kbn, _, moment) {
if (old.services) {
if (old.services.filter) {
this.time = old.services.filter.time;
this.templating.list = old.services.filter.list;
this.templating.list = old.services.filter.list || [];
}
delete this.services;
}

View File

@ -14,9 +14,12 @@ function (angular, _, $) {
// like fullscreen panel & edit
function DashboardViewState($scope) {
var self = this;
self.state = {};
self.panelScopes = [];
self.$scope = $scope;
$scope.exitFullscreen = function() {
if (self.fullscreen) {
if (self.state.fullscreen) {
self.update({ fullscreen: false });
}
};
@ -28,42 +31,48 @@ function (angular, _, $) {
}
});
this.panelScopes = [];
this.$scope = $scope;
this.update(this.getQueryStringState(), true);
}
DashboardViewState.prototype.needsSync = function(urlState) {
if (urlState.fullscreen !== this.fullscreen) { return true; }
if (urlState.edit !== this.edit) { return true; }
if (urlState.panelId !== this.panelId) { return true; }
return false;
return _.isEqual(this.state, urlState) === false;
};
DashboardViewState.prototype.getQueryStringState = function() {
var queryParams = $location.search();
return {
var urlState = {
panelId: parseInt(queryParams.panelId) || null,
fullscreen: queryParams.fullscreen ? true : false,
edit: queryParams.edit ? true : false
edit: queryParams.edit ? true : false,
};
_.each(queryParams, function(value, key) {
if (key.indexOf('var-') !== 0) { return; }
urlState[key] = value;
});
return urlState;
};
DashboardViewState.prototype.serializeToUrl = function() {
var urlState = _.clone(this.state);
urlState.fullscreen = this.state.fullscreen ? true : null,
urlState.edit = this.state.edit ? true : null;
return urlState;
};
DashboardViewState.prototype.update = function(state, skipUrlSync) {
_.extend(this, state);
_.extend(this.state, state);
this.fullscreen = this.state.fullscreen;
if (!this.fullscreen) {
this.panelId = null;
this.edit = false;
if (!this.state.fullscreen) {
this.state.panelId = null;
this.state.edit = false;
}
if (!skipUrlSync) {
$location.search({
fullscreen: this.fullscreen ? true : null,
panelId: this.panelId,
edit: this.edit ? true : null
});
$location.search(this.serializeToUrl());
}
this.syncState();
@ -76,7 +85,7 @@ function (angular, _, $) {
if (this.fullscreenPanel) {
this.leaveFullscreen(false);
}
var panelScope = this.getPanelScope(this.panelId);
var panelScope = this.getPanelScope(this.state.panelId);
this.enterFullscreen(panelScope);
return;
}
@ -118,8 +127,8 @@ function (angular, _, $) {
var fullscreenHeight = Math.floor(docHeight * 0.7);
this.oldTimeRange = panelScope.range;
panelScope.height = this.edit ? editHeight : fullscreenHeight;
panelScope.editMode = this.edit;
panelScope.height = this.state.edit ? editHeight : fullscreenHeight;
panelScope.editMode = this.state.edit;
this.fullscreenPanel = panelScope;
$(window).scrollTop(0);
@ -135,7 +144,7 @@ function (angular, _, $) {
var self = this;
self.panelScopes.push(panelScope);
if (self.panelId === panelScope.panel.id) {
if (self.state.panelId === panelScope.panel.id) {
self.enterFullscreen(panelScope);
}

View File

@ -80,7 +80,7 @@ function (angular, _, config) {
if (!name) { return this.default; }
if (datasources[name]) { return datasources[name]; }
throw "Unable to find datasource: " + name;
return this.default;
};
this.getAnnotationSources = function() {

View File

@ -1,12 +1,11 @@
define([
'angular',
'lodash',
'jquery',
'config',
'kbn',
'moment'
],
function (angular, _, $, config, kbn, moment) {
function (angular, _, config, kbn, moment) {
'use strict';
var module = angular.module('grafana.services');
@ -38,6 +37,7 @@ function (angular, _, $, config, kbn, moment) {
};
if (this.basicAuth) {
options.withCredentials = true;
options.headers = {
"Authorization": "Basic " + this.basicAuth
};
@ -76,57 +76,78 @@ function (angular, _, $, config, kbn, moment) {
var queryInterpolated = templateSrv.replace(queryString);
var filter = { "bool": { "must": [{ "range": range }] } };
var query = { "bool": { "should": [{ "query_string": { "query": queryInterpolated } }] } };
var data = { "query" : { "filtered": { "query" : query, "filter": filter } }, "size": 100 };
var data = {
"fields": [timeField, "_source"],
"query" : { "filtered": { "query" : query, "filter": filter } },
"size": 100
};
return this._request('POST', '/_search', annotation.index, data).then(function(results) {
var list = [];
var hits = results.data.hits.hits;
var getFieldFromSource = function(source, fieldName) {
if (!fieldName) { return; }
var fieldNames = fieldName.split('.');
var fieldValue = source;
for (var i = 0; i < fieldNames.length; i++) {
fieldValue = fieldValue[fieldNames[i]];
}
if (_.isArray(fieldValue)) {
fieldValue = fieldValue.join(', ');
}
return fieldValue;
};
for (var i = 0; i < hits.length; i++) {
var source = hits[i]._source;
var fields = hits[i].fields;
var time = source[timeField];
if (_.isString(fields[timeField]) || _.isNumber(fields[timeField])) {
time = fields[timeField];
}
var event = {
annotation: annotation,
time: moment.utc(source[timeField]).valueOf(),
title: source[titleField],
time: moment.utc(time).valueOf(),
title: getFieldFromSource(source, titleField),
tags: getFieldFromSource(source, tagsField),
text: getFieldFromSource(source, textField)
};
if (source[tagsField]) {
if (_.isArray(source[tagsField])) {
event.tags = source[tagsField].join(', ');
}
else {
event.tags = source[tagsField];
}
}
if (textField && source[textField]) {
event.text = source[textField];
}
list.push(event);
}
return list;
});
};
ElasticDatasource.prototype._getDashboardWithSlug = function(id) {
return this._get('/dashboard/' + kbn.slugifyForUrl(id))
.then(function(result) {
return angular.fromJson(result._source.dashboard);
}, function() {
throw "Dashboard not found";
});
};
ElasticDatasource.prototype.getDashboard = function(id, isTemp) {
var url = '/dashboard/' + id;
if (isTemp) { url = '/temp/' + id; }
if (isTemp) {
url = '/temp/' + id;
}
var self = this;
return this._get(url)
.then(function(result) {
if (result._source && result._source.dashboard) {
return angular.fromJson(result._source.dashboard);
} else {
return false;
}
return angular.fromJson(result._source.dashboard);
}, function(data) {
if(data.status === 0) {
throw "Could not contact Elasticsearch. Please ensure that Elasticsearch is reachable from your browser.";
} else {
throw "Could not find dashboard " + id;
// backward compatible fallback
return self._getDashboardWithSlug(id);
}
});
};
@ -148,15 +169,30 @@ function (angular, _, $, config, kbn, moment) {
return this._saveTempDashboard(data);
}
else {
return this._request('PUT', '/dashboard/' + encodeURIComponent(title), this.index, data)
.then(function() {
return { title: title, url: '/dashboard/db/' + title };
}, function(err) {
throw 'Failed to save to elasticsearch ' + err.data;
var id = encodeURIComponent(kbn.slugifyForUrl(title));
var self = this;
return this._request('PUT', '/dashboard/' + id, this.index, data)
.then(function(results) {
self._removeUnslugifiedDashboard(results, title, id);
return { title: title, url: '/dashboard/db/' + id };
}, function() {
throw 'Failed to save to elasticsearch';
});
}
};
ElasticDatasource.prototype._removeUnslugifiedDashboard = function(saveResult, title, id) {
if (saveResult.statusText !== 'Created') { return; }
if (title === id) { return; }
var self = this;
this._get('/dashboard/' + title).then(function() {
self.deleteDashboard(title);
});
};
ElasticDatasource.prototype._saveTempDashboard = function(data) {
return this._request('POST', '/temp/?ttl=' + this.saveTempTTL, this.index, data)
.then(function(result) {
@ -181,7 +217,21 @@ function (angular, _, $, config, kbn, moment) {
};
ElasticDatasource.prototype.searchDashboards = function(queryString) {
queryString = queryString.toLowerCase().replace(' and ', ' AND ');
var endsInOpen = function(string, opener, closer) {
var character;
var count = 0;
for (var i=0; i<string.length; i++) {
character = string[i];
if (character === opener) {
count++;
} else if (character === closer) {
count--;
}
}
return count > 0;
};
var tagsOnly = queryString.indexOf('tags!:') === 0;
if (tagsOnly) {
@ -193,7 +243,21 @@ function (angular, _, $, config, kbn, moment) {
queryString = 'title:';
}
if (queryString[queryString.length - 1] !== '*') {
// make this a partial search if we're not in some reserved portion of the language, comments on conditionals, in order:
// 1. ends in reserved character, boosting, boolean operator ( -foo)
// 2. typing a reserved word like AND, OR, NOT
// 3. open parens (groupiing)
// 4. open " (term phrase)
// 5. open [ (range)
// 6. open { (range)
// see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/query-dsl-query-string-query.html#query-string-syntax
if (!queryString.match(/(\*|\]|}|~|\)|"|^\d+|\s[\-+]\w+)$/) &&
!queryString.match(/[A-Z]$/) &&
!endsInOpen(queryString, '(', ')') &&
!endsInOpen(queryString, '"', '"') &&
!endsInOpen(queryString, '[', ']') && !endsInOpen(queryString, '[', '}') &&
!endsInOpen(queryString, '{', ']') && !endsInOpen(queryString, '{', '}')
){
queryString += '*';
}
}
@ -216,7 +280,7 @@ function (angular, _, $, config, kbn, moment) {
for (var i = 0; i < results.hits.hits.length; i++) {
hits.dashboards.push({
id: results.hits.hits[i]._id,
title: results.hits.hits[i]._id,
title: results.hits.hits[i]._source.title,
tags: results.hits.hits[i]._source.tags
});
}

View File

@ -68,7 +68,14 @@ function (_) {
addFuncDef({
name: 'diffSeries',
params: optionalSeriesRefArgs,
defaultParams: ['#B'],
defaultParams: ['#A'],
category: categories.Calculate,
});
addFuncDef({
name: 'divideSeries',
params: optionalSeriesRefArgs,
defaultParams: ['#A'],
category: categories.Calculate,
});
@ -79,6 +86,13 @@ function (_) {
category: categories.Calculate,
});
addFuncDef({
name: 'group',
params: optionalSeriesRefArgs,
defaultParams: ['#A', '#B'],
category: categories.Combine,
});
addFuncDef({
name: 'sumSeries',
shortName: 'sum',

View File

@ -19,6 +19,7 @@ function (angular, _, kbn, InfluxSeries, InfluxQueryBuilder) {
this.username = datasource.username;
this.password = datasource.password;
this.name = datasource.name;
this.basicAuth = datasource.basicAuth;
this.saveTemp = _.isUndefined(datasource.save_temp) ? true : datasource.save_temp;
this.saveTempTTL = _.isUndefined(datasource.save_temp_ttl) ? '30d' : datasource.save_temp_ttl;
@ -63,7 +64,7 @@ function (angular, _, kbn, InfluxSeries, InfluxQueryBuilder) {
InfluxDatasource.prototype.annotationQuery = function(annotation, rangeUnparsed) {
var timeFilter = getTimeFilter({ range: rangeUnparsed });
var query = annotation.query.replace('$timeFilter', timeFilter);
query = templateSrv.replace(annotation.query);
query = templateSrv.replace(query);
return this._seriesQuery(query).then(function(results) {
return new InfluxSeries({ seriesList: results, annotation: annotation }).getAnnotations();
@ -170,6 +171,11 @@ function (angular, _, kbn, InfluxSeries, InfluxQueryBuilder) {
inspect: { type: 'influxdb' },
};
options.headers = options.headers || {};
if (_this.basicAuth) {
options.headers.Authorization = 'Basic ' + _this.basicAuth;
}
return $http(options).success(function (data) {
deferred.resolve(data);
});
@ -182,34 +188,46 @@ function (angular, _, kbn, InfluxSeries, InfluxQueryBuilder) {
var tags = dashboard.tags.join(',');
var title = dashboard.title;
var temp = dashboard.temp;
var id = kbn.slugifyForUrl(title);
if (temp) { delete dashboard.temp; }
var data = [{
name: 'grafana.dashboard_' + btoa(title),
columns: ['time', 'sequence_number', 'title', 'tags', 'dashboard'],
points: [[1000000000000, 1, title, tags, angular.toJson(dashboard)]]
name: 'grafana.dashboard_' + btoa(id),
columns: ['time', 'sequence_number', 'title', 'tags', 'dashboard', 'id'],
points: [[1000000000000, 1, title, tags, angular.toJson(dashboard), id]]
}];
if (temp) {
return this._saveDashboardTemp(data, title);
return this._saveDashboardTemp(data, title, id);
}
else {
var self = this;
return this._influxRequest('POST', '/series', data).then(function() {
return { title: title, url: '/dashboard/db/' + title };
self._removeUnslugifiedDashboard(title, false);
return { title: title, url: '/dashboard/db/' + id };
}, function(err) {
throw 'Failed to save dashboard to InfluxDB: ' + err.data;
});
}
};
InfluxDatasource.prototype._saveDashboardTemp = function(data, title) {
data[0].name = 'grafana.temp_dashboard_' + btoa(title);
InfluxDatasource.prototype._removeUnslugifiedDashboard = function(id, isTemp) {
var self = this;
self._getDashboardInternal(id, isTemp).then(function(dashboard) {
if (dashboard !== null) {
self.deleteDashboard(id);
}
});
};
InfluxDatasource.prototype._saveDashboardTemp = function(data, title, id) {
data[0].name = 'grafana.temp_dashboard_' + btoa(id);
data[0].columns.push('expires');
data[0].points[0].push(this._getTempDashboardExpiresDate());
return this._influxRequest('POST', '/series', data).then(function() {
var baseUrl = window.location.href.replace(window.location.hash,'');
var url = baseUrl + "#dashboard/temp/" + title;
var url = baseUrl + "#dashboard/temp/" + id;
return { title: title, url: url };
}, function(err) {
throw 'Failed to save shared dashboard to InfluxDB: ' + err.data;
@ -236,7 +254,7 @@ function (angular, _, kbn, InfluxSeries, InfluxQueryBuilder) {
return expires;
};
InfluxDatasource.prototype.getDashboard = function(id, isTemp) {
InfluxDatasource.prototype._getDashboardInternal = function(id, isTemp) {
var queryString = 'select dashboard from "grafana.dashboard_' + btoa(id) + '"';
if (isTemp) {
@ -245,15 +263,34 @@ function (angular, _, kbn, InfluxSeries, InfluxQueryBuilder) {
return this._seriesQuery(queryString).then(function(results) {
if (!results || !results.length) {
throw "Dashboard not found";
return null;
}
var dashCol = _.indexOf(results[0].columns, 'dashboard');
var dashJson = results[0].points[0][dashCol];
return angular.fromJson(dashJson);
}, function() {
return null;
});
};
InfluxDatasource.prototype.getDashboard = function(id, isTemp) {
var self = this;
return this._getDashboardInternal(id, isTemp).then(function(dashboard) {
if (dashboard !== null) {
return dashboard;
}
// backward compatible load for unslugified ids
var slug = kbn.slugifyForUrl(id);
if (slug !== id) {
return self.getDashboard(slug, isTemp);
}
throw "Dashboard not found";
}, function(err) {
return "Could not load dashboard, " + err.data;
throw "Could not load dashboard, " + err.data;
});
};
@ -264,12 +301,12 @@ function (angular, _, kbn, InfluxSeries, InfluxQueryBuilder) {
}
return id;
}, function(err) {
return "Could not delete dashboard, " + err.data;
throw "Could not delete dashboard, " + err.data;
});
};
InfluxDatasource.prototype.searchDashboards = function(queryString) {
var influxQuery = 'select title, tags from /grafana.dashboard_.*/ where ';
var influxQuery = 'select * from /grafana.dashboard_.*/ where ';
var tagsOnly = queryString.indexOf('tags!:') === 0;
if (tagsOnly) {
@ -294,15 +331,21 @@ function (angular, _, kbn, InfluxSeries, InfluxQueryBuilder) {
return hits;
}
var dashCol = _.indexOf(results[0].columns, 'title');
var tagsCol = _.indexOf(results[0].columns, 'tags');
for (var i = 0; i < results.length; i++) {
var dashCol = _.indexOf(results[i].columns, 'title');
var tagsCol = _.indexOf(results[i].columns, 'tags');
var idCol = _.indexOf(results[i].columns, 'id');
var hit = {
id: results[i].points[0][dashCol],
title: results[i].points[0][dashCol],
tags: results[i].points[0][tagsCol].split(",")
};
if (idCol !== -1) {
hit.id = results[i].points[0][idCol];
}
hit.tags = hit.tags[0] ? hit.tags : [];
hits.dashboards.push(hit);
}

View File

@ -19,7 +19,7 @@ function (angular, _, kbn) {
}
// Called once per panel (graph)
OpenTSDBDatasource.prototype.query = function(filterSrv, options) {
OpenTSDBDatasource.prototype.query = function(options) {
var start = convertToTSDBTime(options.range.from);
var end = convertToTSDBTime(options.range.to);
var queries = _.compact(_.map(options.targets, convertTargetToQuery));

View File

@ -7,37 +7,29 @@ function (angular, _) {
var module = angular.module('grafana.services');
module.service('templateSrv', function($q, $routeParams) {
module.service('templateSrv', function() {
var self = this;
this._regex = /\$(\w+)|\[\[([\s\S]+?)\]\]/g;
this._templateData = {};
this._values = {};
this._texts = {};
this._grafanaVariables = {};
this.init = function(variables) {
this.variables = variables;
this.updateTemplateData(true);
this.updateTemplateData();
};
this.updateTemplateData = function(initial) {
var data = {};
this.updateTemplateData = function() {
this._values = {};
this._texts = {};
_.each(this.variables, function(variable) {
if (initial) {
var urlValue = $routeParams[ variable.name ];
if (urlValue) {
variable.current = { text: urlValue, value: urlValue };
}
}
if (!variable.current || !variable.current.value) { return; }
if (!variable.current || !variable.current.value) {
return;
}
data[variable.name] = variable.current.value;
});
this._templateData = data;
this._values[variable.name] = variable.current.value;
this._texts[variable.name] = variable.current.text;
}, this);
};
this.setGrafanaVariable = function (name, value) {
@ -47,7 +39,11 @@ function (angular, _) {
this.variableExists = function(expression) {
this._regex.lastIndex = 0;
var match = this._regex.exec(expression);
return match && (self._templateData[match[1] || match[2]] !== void 0);
return match && (self._values[match[1] || match[2]] !== void 0);
};
this.containsVariable = function(str, variableName) {
return str.indexOf('$' + variableName) !== -1 || str.indexOf('[[' + variableName + ']]') !== -1;
};
this.highlightVariablesAsHtml = function(str) {
@ -55,7 +51,7 @@ function (angular, _) {
this._regex.lastIndex = 0;
return str.replace(this._regex, function(match, g1, g2) {
if (self._templateData[g1 || g2]) {
if (self._values[g1 || g2]) {
return '<span class="template-variable">' + match + '</span>';
}
return match;
@ -69,13 +65,29 @@ function (angular, _) {
this._regex.lastIndex = 0;
return target.replace(this._regex, function(match, g1, g2) {
value = self._templateData[g1 || g2];
value = self._values[g1 || g2];
if (!value) { return match; }
return self._grafanaVariables[value] || value;
});
};
this.replaceWithText = function(target) {
if (!target) { return; }
var value;
var text;
this._regex.lastIndex = 0;
return target.replace(this._regex, function(match, g1, g2) {
value = self._values[g1 || g2];
text = self._texts[g1 || g2];
if (!value) { return match; }
return self._grafanaVariables[value] || text;
});
};
});
});

View File

@ -18,17 +18,24 @@ function (angular, _, kbn) {
}
});
this.init = function(dashboard) {
this.init = function(dashboard, viewstate) {
this.variables = dashboard.templating.list;
this.viewstate = viewstate;
templateSrv.init(this.variables);
for (var i = 0; i < this.variables.length; i++) {
var param = this.variables[i];
if (param.refresh) {
this.updateOptions(param);
var variable = this.variables[i];
var urlValue = viewstate.state['var-' + variable.name];
if (urlValue !== void 0) {
var option = _.findWhere(variable.options, { text: urlValue });
option = option || { text: urlValue, value: urlValue };
this.setVariableValue(variable, option, true);
}
else if (param.type === 'interval') {
this.updateAutoInterval(param);
else if (variable.refresh) {
this.updateOptions(variable);
}
else if (variable.type === 'interval') {
this.updateAutoInterval(variable);
}
}
};
@ -63,7 +70,7 @@ function (angular, _, kbn) {
if (otherVariable === updatedVariable) {
return;
}
if (otherVariable.query.indexOf('[[' + updatedVariable.name + ']]') !== -1) {
if (templateSrv.containsVariable(otherVariable.query, updatedVariable.name)) {
return self.updateOptions(otherVariable);
}
});
@ -92,7 +99,6 @@ function (angular, _, kbn) {
var datasource = datasourceSrv.get(variable.datasource);
return datasource.metricFindQuery(variable.query)
.then(function (results) {
variable.options = self.metricNamesToVariableValues(variable, results);
if (variable.includeAll) {
@ -102,9 +108,9 @@ function (angular, _, kbn) {
// if parameter has current value
// if it exists in options array keep value
if (variable.current) {
var currentExists = _.findWhere(variable.options, { value: variable.current.value });
if (currentExists) {
return self.setVariableValue(variable, variable.current, true);
var currentOption = _.findWhere(variable.options, { text: variable.current.text });
if (currentOption) {
return self.setVariableValue(variable, currentOption, true);
}
}

View File

@ -2,13 +2,14 @@ define([
'angular',
'lodash',
'config',
'kbn'
], function (angular, _, config, kbn) {
'kbn',
'moment'
], function (angular, _, config, kbn, moment) {
'use strict';
var module = angular.module('grafana.services');
module.service('timeSrv', function($rootScope, $timeout, timer) {
module.service('timeSrv', function($rootScope, $timeout, $routeParams, timer) {
var self = this;
this.init = function(dashboard) {
@ -17,11 +18,40 @@ define([
this.dashboard = dashboard;
this.time = dashboard.time;
this._initTimeFromUrl();
if(this.dashboard.refresh) {
this.set_interval(this.dashboard.refresh);
}
};
this._parseUrlParam = function(value) {
if (value.indexOf('now') !== -1) {
return value;
}
if (value.length === 8) {
return moment.utc(value, 'YYYYMMDD').toDate();
}
if (value.length === 15) {
return moment.utc(value, 'YYYYMMDDTHHmmss').toDate();
}
var epoch = parseInt(value);
if (!_.isNaN(epoch)) {
return new Date(epoch);
}
return null;
};
this._initTimeFromUrl = function() {
if ($routeParams.from) {
this.time.from = this._parseUrlParam($routeParams.from) || this.time.from;
}
if ($routeParams.to) {
this.time.to = this._parseUrlParam($routeParams.to) || this.time.to;
}
};
this.set_interval = function (interval) {
this.dashboard.refresh = interval;
if (interval) {

View File

@ -83,6 +83,14 @@ function(angular, _, config) {
current.time = original.time = {};
current.refresh = original.refresh;
// ignore template variable values
_.each(current.templating.list, function(value, index) {
value.current = null;
value.options = null;
original.templating.list[index].current = null;
original.templating.list[index].options = null;
});
var currentTimepicker = _.findWhere(current.nav, { type: 'timepicker' });
var originalTimepicker = _.findWhere(original.nav, { type: 'timepicker' });

View File

@ -1,8 +1,7 @@
///// @scratch /configuration/config.js/1
// == Configuration
// config.js is where you will find the core Grafana configuration. This file contains parameter that
// must be set before Grafana is run for the first time.
///
// == Configuration
// config.js is where you will find the core Grafana configuration. This file contains parameter that
// must be set before Grafana is run for the first time.
define(['settings'],
function (Settings) {
"use strict";
@ -97,7 +96,7 @@ function (Settings) {
// Change window title prefix from 'Grafana - <dashboard title>'
window_title_prefix: 'Grafana - ',
// Add your own custom pannels
// Add your own custom panels
plugins: {
// list of plugin panels
panels: [],

View File

@ -56,7 +56,7 @@ hr {
}
.brand {
padding: 15px 20px 15px;
padding: 0px 15px;
color: @grayLighter;
font-weight: normal;
text-shadow: none;

View File

@ -59,6 +59,7 @@ a.text-success:hover { color: darken(@green, 10%); }
}
.brand {
padding: 0px 15px;
&:hover {
color: @navbarLinkColorHover;
@ -461,7 +462,6 @@ legend {
// -----------------------------------------------------
.alert {
.border-radius(0);
text-shadow: none;
&-heading, h1, h2, h3, h4, h5, h6 {

View File

@ -35,6 +35,19 @@
}
}
.logo-icon {
width: 24px;
padding: 13px 11px 0 0;
display: block;
float: left;
}
.page-title {
padding: 15px 0;
display: block;
float: left;
}
.row-button {
width: 24px;
}
@ -87,7 +100,7 @@
.panel-fullscreen {
z-index: 100;
display: block !important;
display: block;
position: fixed;
left: 0px;
right: 0px;
@ -103,11 +116,10 @@
}
}
.dashboard-fullscreen .main-view-container {
height: 0px;
width: 0px;
position: fixed;
right: -10000px;
.dashboard-fullscreen {
.row-control-inner {
display: none;
}
}
.histogram-chart {
@ -423,6 +435,9 @@ select.grafana-target-segment-input {
background-color: rgb(58, 57, 57);
border-radius: 5px;
z-index: 9999;
max-width: 800px;
max-height: 600px;
overflow: hidden;
}
.tooltip.in {
@ -485,3 +500,26 @@ select.grafana-target-segment-input {
color: @variable;
}
.grafana-info-box:before {
content: "\f05a";
font-family:'FontAwesome';
position: absolute;
top: -8px;
left: -8px;
font-size: 20px;
color: @blue;
}
.grafana-info-box {
position: relative;
padding: 5px 15px;
background-color: @grafanaTargetBackground;
border: 1px solid @grafanaTargetBorder;
h5 {
margin-top: 5px;
}
}
.grafana-tip {
padding-left: 5px;
}

View File

@ -317,13 +317,38 @@ div.flot-text {
color: @textColor !important;
}
.dashboard-notice {
.page-alert-list {
z-index:8000;
margin-left:0px;
padding:3px 0px 3px 0px;
width:100%;
padding-left:20px;
color: @white;
min-width: 300px;
max-width: 300px;
position: fixed;
right: 20px;
top: 56px;
.alert {
color: @white;
padding-bottom: 13px;
position: relative;
}
.alert-close {
position: absolute;
top: -4px;
right: -2px;
width: 19px;
height: 19px;
padding: 0;
background: @grayLighter;
border-radius: 50%;
border: none;
font-size: 1.1rem;
color: @grayDarker;
}
.alert-title {
font-weight: bold;
padding-bottom: 2px;
}
}
@ -516,6 +541,11 @@ div.flot-text {
}
}
// typeahead max height
.typeahead {
max-height: 300px;
overflow-y: auto;
}
// Labels & Badges
.label-tag {

View File

@ -257,7 +257,7 @@
// Form states and alerts
// -------------------------
@warningText: darken(#c09853, 10%);
@warningBackground: @grayLighter;
@warningBackground: @orange;
@warningBorder: transparent;
@errorText: #b94a48;

BIN
src/img/fav16.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

BIN
src/img/fav32.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

BIN
src/img/fav_dark_16.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

BIN
src/img/fav_dark_32.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

View File

@ -5,9 +5,11 @@
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<meta name="viewport" content="width=device-width">
<meta name="google" value="notranslate">
<title>Grafana</title>
<link rel="stylesheet" href="css/grafana.dark.min.css" title="Dark">
<link rel="icon" type="image/png" href="img/fav32.png">
<!-- build:js app/app.js -->
<script src="vendor/require/require.js"></script>
@ -22,12 +24,17 @@
<link rel="stylesheet" href="css/grafana.light.min.css" ng-if="grafana.style === 'light'">
<div ng-repeat='alert in dashAlerts.list' class="alert-{{alert.severity}} dashboard-notice" ng-show="$last">
<button type="button" class="close" ng-click="dashAlerts.clear(alert)" style="padding-right:50px">&times;</button>
<strong>{{alert.title}}</strong> <span ng-bind-html='alert.text'></span> <div style="padding-right:10px" class='pull-right small'> {{$index + 1}} alert(s) </div>
</div>
<div class="page-alert-list">
<div ng-repeat='alert in dashAlerts.list' class="alert-{{alert.severity}} alert">
<button type="button" class="alert-close" ng-click="dashAlerts.clear(alert)">
<i class="icon-remove-sign"></i>
</button>
<div class="alert-title">{{alert.title}}</div>
<div ng-bind-html='alert.text'></div>
</div>
</div>
<div ng-view></div>
<div ng-view></div>
</body>
</body>
</html>

View File

@ -20,6 +20,7 @@ define([
viewState.update(updateState);
expect(location.search()).to.eql(updateState);
expect(viewState.fullscreen).to.be(true);
expect(viewState.state.fullscreen).to.be(true);
});
});
@ -29,6 +30,7 @@ define([
viewState.update({fullscreen: false});
expect(location.search()).to.eql({});
expect(viewState.fullscreen).to.be(false);
expect(viewState.state.fullscreen).to.be(false);
});
});

View File

@ -99,6 +99,25 @@ define([
});
describe('when initializing a target with single param func using variable', function() {
beforeEach(function() {
ctx.scope.target.target = 'movingAverage(prod.count, $var)';
ctx.scope.datasource.metricFindQuery.returns(ctx.$q.when([]));
ctx.scope.init();
ctx.scope.$digest();
ctx.scope.$parent = { get_data: sinon.spy() };
});
it('should add 2 segments', function() {
expect(ctx.scope.segments.length).to.be(2);
});
it('should add function param', function() {
expect(ctx.scope.functions[0].params.length).to.be(1);
});
});
describe('when initalizing target without metric expression and function with series-ref', function() {
beforeEach(function() {
ctx.scope.target.target = 'asPercent(metric.node.count, #A)';

View File

@ -51,6 +51,7 @@ define([
self.templateSrv = new TemplateSrvStub();
self.timeSrv = new TimeSrvStub();
self.datasourceSrv = {};
self.$routeParams = {};
this.providePhase = function(mocks) {
return module(function($provide) {
@ -103,6 +104,7 @@ define([
this.replace = function(text) {
return _.template(text, this.data, this.templateSettings);
};
this.init = function() {};
this.updateTemplateData = function() { };
this.variableExists = function() { return false; };
this.highlightVariablesAsHtml = function(str) { return str; };

View File

@ -8,7 +8,7 @@ define([
var ctx = new helpers.ServiceTestContext();
beforeEach(module('grafana.services'));
beforeEach(ctx.providePhase());
beforeEach(ctx.providePhase(['templateSrv']));
beforeEach(ctx.createService('InfluxDatasource'));
beforeEach(function() {
ctx.ds = new ctx.service({ urls: [''], user: 'test', password: 'mupp' });
@ -70,6 +70,30 @@ define([
});
describe('When issuing annotation query', function() {
var results;
var urlExpected = "/series?p=mupp&q=select+title+from+events.backend_01"+
"+where+time+%3E+now()+-+1h&time_precision=s";
var range = { from: 'now-1h', to: 'now' };
var annotation = { query: 'select title from events.$server where $timeFilter' };
var response = [];
beforeEach(function() {
ctx.templateSrv.replace = function(str) {
return str.replace('$server', 'backend_01');
};
ctx.$httpBackend.expect('GET', urlExpected).respond(response);
ctx.ds.annotationQuery(annotation, range).then(function(data) { results = data; });
ctx.$httpBackend.flush();
});
it('should generate the correct query', function() {
ctx.$httpBackend.verifyNoOutstandingExpectation();
});
});
});
});

View File

@ -15,6 +15,12 @@ define([
expect(str).to.be('1.02 hour');
});
it('should not downscale when value is zero', function() {
var str = kbn.msFormat(0, 2);
expect(str).to.be('0.00 ms');
});
it('should translate 365445 as 6.09 min', function() {
var str = kbn.msFormat(365445, 2);
expect(str).to.be('6.09 min');

View File

@ -62,6 +62,24 @@ define([
});
describe('when checking if a string contains a variable', function() {
beforeEach(function() {
_templateSrv.init([{ name: 'test', current: { value: 'muuuu' } }]);
_templateSrv.updateTemplateData();
});
it('should find it with $var syntax', function() {
var contains = _templateSrv.containsVariable('this.$test.filters', 'test');
expect(contains).to.be(true);
});
it('should find it with [[var]] syntax', function() {
var contains = _templateSrv.containsVariable('this.[[test]].filters', 'test');
expect(contains).to.be(true);
});
});
describe('updateTemplateData with simple value', function() {
beforeEach(function() {
_templateSrv.init([{ name: 'test', current: { value: 'muuuu' } }]);
@ -74,6 +92,23 @@ define([
});
});
describe('replaceWithText', function() {
beforeEach(function() {
_templateSrv.init([
{ name: 'server', current: { value: '{asd,asd2}', text: 'All' } },
{ name: 'period', current: { value: '$__auto_interval', text: 'auto' } }
]);
_templateSrv.setGrafanaVariable('$__auto_interval', '13m');
_templateSrv.updateTemplateData();
});
it('should replace with text except for grafanaVariables', function() {
var target = _templateSrv.replaceWithText('Server: $server, period: $period');
expect(target).to.be('Server: All, period: 13m');
});
});
});
});

View File

@ -10,7 +10,7 @@ define([
var ctx = new helpers.ServiceTestContext();
beforeEach(module('grafana.services'));
beforeEach(ctx.providePhase(['datasourceSrv', 'timeSrv', 'templateSrv']));
beforeEach(ctx.providePhase(['datasourceSrv', 'timeSrv', 'templateSrv', "$routeParams"]));
beforeEach(ctx.createService('templateValuesSrv'));
describe('update interval variable options', function() {
@ -125,12 +125,12 @@ define([
describeUpdateVariable('and existing value still exists in options', function(scenario) {
scenario.setup(function() {
scenario.variable = { type: 'query', query: 'apps.*', name: 'test' };
scenario.variable.current = { value: 'backend2'};
scenario.variable.current = { text: 'backend2'};
scenario.queryResult = [{text: 'backend1'}, {text: 'backend2'}];
});
it('should keep variable value', function() {
expect(scenario.variable.current.value).to.be('backend2');
expect(scenario.variable.current.text).to.be('backend2');
});
});
@ -182,18 +182,6 @@ define([
});
});
describeUpdateVariable('and existing value still exists in options', function(scenario) {
scenario.setup(function() {
scenario.variable = { type: 'query', query: 'apps.*', name: 'test' };
scenario.variable.current = { value: 'backend2'};
scenario.queryResult = [{text: 'backend1'}, {text: 'backend2'}];
});
it('should keep variable value', function() {
expect(scenario.variable.current.value).to.be('backend2');
});
});
describeUpdateVariable('with include All glob syntax', function(scenario) {
scenario.setup(function() {
scenario.variable = { type: 'query', query: 'apps.*', name: 'test', includeAll: true, allFormat: 'glob' };

View File

@ -11,7 +11,7 @@ define([
var _dashboard;
beforeEach(module('grafana.services'));
beforeEach(ctx.providePhase());
beforeEach(ctx.providePhase(['$routeParams']));
beforeEach(ctx.createService('timeSrv'));
beforeEach(function() {
@ -35,6 +35,45 @@ define([
});
});
describe('init time from url', function() {
it('should handle relative times', function() {
ctx.$routeParams.from = 'now-2d';
ctx.$routeParams.to = 'now';
ctx.service.init(_dashboard);
var time = ctx.service.timeRange(false);
expect(time.from).to.be('now-2d');
expect(time.to).to.be('now');
});
it('should handle formated dates', function() {
ctx.$routeParams.from = '20140410T052010';
ctx.$routeParams.to = '20140520T031022';
ctx.service.init(_dashboard);
var time = ctx.service.timeRange(true);
expect(time.from.getTime()).to.equal(new Date("2014-04-10T05:20:10Z").getTime());
expect(time.to.getTime()).to.equal(new Date("2014-05-20T03:10:22Z").getTime());
});
it('should handle formated dates without time', function() {
ctx.$routeParams.from = '20140410';
ctx.$routeParams.to = '20140520';
ctx.service.init(_dashboard);
var time = ctx.service.timeRange(true);
expect(time.from.getTime()).to.equal(new Date("2014-04-10T00:00:00Z").getTime());
expect(time.to.getTime()).to.equal(new Date("2014-05-20T00:00:00Z").getTime());
});
it('should handle epochs', function() {
ctx.$routeParams.from = '1410337646373';
ctx.$routeParams.to = '1410337665699';
ctx.service.init(_dashboard);
var time = ctx.service.timeRange(true);
expect(time.from.getTime()).to.equal(1410337646373);
expect(time.to.getTime()).to.equal(1410337665699);
});
});
describe('setTime', function() {
it('should return disable refresh for absolute times', function() {
_dashboard.refresh = false;

View File

@ -1,4 +1,6 @@
module.exports = function(config,grunt) {
'use strict';
var _c = {
build: {
options: {
@ -59,12 +61,15 @@ module.exports = function(config,grunt) {
'directives/all',
'jquery.flot.pie',
'angular-dragdrop',
'controllers/all',
'routes/all',
'components/partials',
]
}
];
var fs = require('fs');
var panelPath = config.srcDir+'/app/panels'
var panelPath = config.srcDir+'/app/panels';
// create a module for each directory in src/app/panels/
fs.readdirSync(panelPath).forEach(function (panelName) {