diff --git a/' b/' new file mode 100644 index 00000000000..86791d43fde --- /dev/null +++ b/' @@ -0,0 +1,84 @@ +/* global _ */ + +/* + * Complex scripted dashboard + * This script generates a dashboard object that Grafana can load. It also takes a number of user + * supplied URL parameters (int ARGS variable) + * + * Return a dashboard object, or a function + * + * For async scripts, return a function, this function must take a single callback function as argument, + * call this callback function with the dashboard object (look at scripted_async.js for an example) + */ + +'use strict'; + +// accessable variables in this scope +var window, document, ARGS, $, jQuery, moment, kbn, services, _; + +// default datasource +var datasource = services.datasourceSrv.default; +// get datasource used for saving dashboards +var dashboardDB = services.datasourceSrv.getGrafanaDB(); + +var targets = []; + +function getTargets(path) { + return datasource.metricFindQuery(path + '.*').then(function(result) { + if (!result) { + return null; + } + + if (targets.length === 10) { + return null; + } + + var promises = _.map(result, function(metric) { + if (metric.expandable) { + return getTargets(path + "." + metric.text); + } + else { + targets.push(path + '.' + metric.text); + } + return null; + }); + + return services.$q.when(promises); + }); +} + +function createDashboard(target, index) { + // Intialize a skeleton with nothing but a rows array and service object + var dashboard = { rows : [] }; + dashboard.title = 'Scripted dash ' + index; + dashboard.time = { + from: "now-6h", + to: "now" + }; + + dashboard.rows.push({ + title: 'Chart', + height: '300px', + panels: [ + { + title: 'Events', + type: 'graph', + span: 12, + targets: [ {target: target} ] + } + ] + }); + +} + +return function(callback) { + + getTargets('apps').then(function(results) { + console.log('targets: ', targets); + _.each(targets, function(target, index) { + var dashboard = createDashboard(target); + }); + }); + +}; + diff --git a/CHANGELOG.md b/CHANGELOG.md index dd27a6a6667..24772741c14 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,10 +11,15 @@ **Misc** - [Issue #938](https://github.com/grafana/grafana/issues/938). Panel: Plugin panels now reside outside of app/panels directory - [Issue #952](https://github.com/grafana/grafana/issues/952). Help: Shortcut "?" to open help modal with list of all shortcuts +- [Issue #991](https://github.com/grafana/grafana/issues/991). ScriptedDashboard: datasource services are now available in scripted dashboards, you can query datasource for metric keys, generate dashboards, and even save them in a scripted dashboard (see scripted_gen_and_save.js for example) + +**OpenTSDB** +- [Issue #930](https://github.com/grafana/grafana/issues/930). OpenTSDB: Adding counter max and counter reset value to open tsdb query editor, thx @rsimiciuc **Fixes** - [Issue #925](https://github.com/grafana/grafana/issues/925). Graph: bar width calculation fix for some edge cases (bars would render on top of each other) - [Issue #505](https://github.com/grafana/grafana/issues/505). Graph: fix for second y axis tick unit labels wrapping on the next line +- [Issue #987](https://github.com/grafana/grafana/issues/987). Dashboard: Collapsed rows became invisible when hide controls was enabled ======= # 1.8.1 (2014-09-30) diff --git a/src/app/components/require.config.js b/src/app/components/require.config.js index 5d8da32846f..882583083c6 100644 --- a/src/app/components/require.config.js +++ b/src/app/components/require.config.js @@ -3,6 +3,7 @@ */ require.config({ baseUrl: 'app', + urlArgs: 'bust=' + (new Date().getTime()), paths: { config: ['../config', '../config.sample'], diff --git a/src/app/dashboards/scripted_gen_and_save.js b/src/app/dashboards/scripted_gen_and_save.js new file mode 100644 index 00000000000..d874b3fc28e --- /dev/null +++ b/src/app/dashboards/scripted_gen_and_save.js @@ -0,0 +1,95 @@ +/* global _ */ + +/* + * Complex scripted dashboard + * This script generates a dashboard object that Grafana can load. It also takes a number of user + * supplied URL parameters (int ARGS variable) + * + * Return a dashboard object, or a function + * + * For async scripts, return a function, this function must take a single callback function as argument, + * call this callback function with the dashboard object (look at scripted_async.js for an example) + */ + +'use strict'; + +// accessable variables in this scope +var window, document, ARGS, $, jQuery, moment, kbn, services, _; + +// default datasource +var datasource = services.datasourceSrv.default; +// get datasource used for saving dashboards +var dashboardDB = services.datasourceSrv.getGrafanaDB(); + +var targets = []; + +function getTargets(path) { + return datasource.metricFindQuery(path + '.*').then(function(result) { + if (!result) { + return null; + } + + if (targets.length === 10) { + return null; + } + + var promises = _.map(result, function(metric) { + if (metric.expandable) { + return getTargets(path + "." + metric.text); + } + else { + targets.push(path + '.' + metric.text); + } + return null; + }); + + return services.$q.all(promises); + }); +} + +function createDashboard(target, index) { + // Intialize a skeleton with nothing but a rows array and service object + var dashboard = { rows : [] }; + dashboard.title = 'Scripted dash ' + index; + dashboard.time = { + from: "now-6h", + to: "now" + }; + + dashboard.rows.push({ + title: 'Chart', + height: '300px', + panels: [ + { + title: 'Events', + type: 'graph', + span: 12, + targets: [ {target: target} ] + } + ] + }); + + return dashboard; +} + +function saveDashboard(dashboard) { + var model = services.dashboardSrv.create(dashboard); + dashboardDB.saveDashboard(model); +} + +return function(callback) { + + getTargets('apps').then(function() { + console.log('targets: ', targets); + _.each(targets, function(target, index) { + var dashboard = createDashboard(target, index); + saveDashboard(dashboard); + + if (index === targets.length - 1) { + callback(dashboard); + } + }); + }); + +}; + diff --git a/src/app/directives/grafanaGraph.tooltip.js b/src/app/directives/grafanaGraph.tooltip.js index 3a4a9b1ac2a..e0fede2f62d 100644 --- a/src/app/directives/grafanaGraph.tooltip.js +++ b/src/app/directives/grafanaGraph.tooltip.js @@ -144,8 +144,9 @@ function ($) { hoverInfo = seriesHoverInfo[i]; value = series.formatValue(hoverInfo.value); - group = ' ' + series.label; - seriesHtml = group + ': ' + value + '
' + seriesHtml; + seriesHtml += '
'; + seriesHtml += ' ' + series.label + ':
'; + seriesHtml += '
' + value + '
'; plot.highlight(i, hoverInfo.hoverIndex); } @@ -154,7 +155,8 @@ function ($) { // single series tooltip else if (item) { series = seriesList[item.seriesIndex]; - group = ' ' + series.label; + group = '
'; + group += ' ' + series.label + ':
'; if (scope.panel.stack && scope.panel.tooltip.value_type === 'individual') { value = item.datapoint[1] - item.datapoint[2]; @@ -165,7 +167,7 @@ function ($) { value = series.formatValue(value); timestamp = dashboard.formatDate(item.datapoint[0]); - group += ': ' + value + '
'; + group += '
' + value + '
'; self.showTooltip(timestamp, group, pos); } diff --git a/src/app/panels/graph/module.js b/src/app/panels/graph/module.js index da8e136ff6d..b9c74db8870 100644 --- a/src/app/panels/graph/module.js +++ b/src/app/panels/graph/module.js @@ -202,6 +202,7 @@ function (angular, app, $, _, kbn, moment, TimeSeries) { $scope.panelMeta.loading = false; $scope.panelMeta.error = err.message || "Timeseries data request error"; $scope.inspector.error = err; + $scope.legend = []; $scope.render([]); }); }; diff --git a/src/app/partials/opentsdb/editor.html b/src/app/partials/opentsdb/editor.html index b74b1d7a269..51a575d933c 100644 --- a/src/app/partials/opentsdb/editor.html +++ b/src/app/partials/opentsdb/editor.html @@ -89,6 +89,32 @@ ng-model="target.isCounter" ng-change="targetBlur()"> +
  • + Counter Max: +
  • +
  • + +
  • +
  • + Counter Reset Value: +
  • +
  • + +
  • Alias:
  • diff --git a/src/app/routes/dashboard-from-script.js b/src/app/routes/dashboard-from-script.js index fa3abd36e81..b73a49b0cbe 100644 --- a/src/app/routes/dashboard-from-script.js +++ b/src/app/routes/dashboard-from-script.js @@ -16,21 +16,28 @@ function (angular, $, config, _, kbn, moment) { .when('/dashboard/script/:jsFile', { templateUrl: 'app/partials/dashboard.html', controller : 'DashFromScriptProvider', + reloadOnSearch: false, }); }); - module.controller('DashFromScriptProvider', function($scope, $rootScope, $http, $routeParams, alertSrv, $q) { + module.controller('DashFromScriptProvider', function($scope, $rootScope, $http, $routeParams, $q, dashboardSrv, datasourceSrv, $timeout) { var execute_script = function(result) { + var services = { + dashboardSrv: dashboardSrv, + datasourceSrv: datasourceSrv, + $q: $q, + }; + /*jshint -W054 */ - var script_func = new Function('ARGS','kbn','_','moment','window','document','$','jQuery', result.data); - var script_result = script_func($routeParams, kbn, _ , moment, window, document, $, $); + var script_func = new Function('ARGS','kbn','_','moment','window','document','$','jQuery', 'services', result.data); + var script_result = script_func($routeParams, kbn, _ , moment, window, document, $, $, services); // Handle async dashboard scripts if (_.isFunction(script_result)) { var deferred = $q.defer(); script_result(function(dashboard) { - $rootScope.$apply(function() { + $timeout(function() { deferred.resolve({ data: dashboard }); }); }); @@ -47,7 +54,7 @@ function (angular, $, config, _, kbn, moment) { .then(execute_script) .then(null,function(err) { console.log('Script dashboard error '+ err); - alertSrv.set('Error', "Could not load scripts/"+file+". Please make sure it exists and returns a valid dashboard", 'error'); + $scope.appEvent('alert-error', ["Script Error", "Please make sure it exists and returns a valid dashboard"]); return false; }); }; diff --git a/src/app/services/influxdb/influxQueryBuilder.js b/src/app/services/influxdb/influxQueryBuilder.js index eb5334fd4e1..5ee7746af6b 100644 --- a/src/app/services/influxdb/influxQueryBuilder.js +++ b/src/app/services/influxdb/influxQueryBuilder.js @@ -18,7 +18,7 @@ function () { var query = 'select '; var seriesName = target.series; - if(!seriesName.match('^/.*/')) { + if(!seriesName.match('^/.*/') && !seriesName.match(/^merge\(.*\)/)) { seriesName = '"' + seriesName+ '"'; } diff --git a/src/app/services/influxdb/influxdbDatasource.js b/src/app/services/influxdb/influxdbDatasource.js index 1f44dbe78ef..287e2286f07 100644 --- a/src/app/services/influxdb/influxdbDatasource.js +++ b/src/app/services/influxdb/influxdbDatasource.js @@ -44,7 +44,7 @@ function (angular, _, kbn, InfluxSeries, InfluxQueryBuilder) { // replace grafana variables query = query.replace('$timeFilter', timeFilter); - query = query.replace('$interval', (target.interval || options.interval)); + query = query.replace(/\$interval/g, (target.interval || options.interval)); // replace templated variables query = templateSrv.replace(query); diff --git a/src/app/services/opentsdb/opentsdbDatasource.js b/src/app/services/opentsdb/opentsdbDatasource.js index f6a36c6f035..17232d54047 100644 --- a/src/app/services/opentsdb/opentsdbDatasource.js +++ b/src/app/services/opentsdb/opentsdbDatasource.js @@ -1,14 +1,15 @@ define([ 'angular', 'lodash', - 'kbn' + 'kbn', + 'moment' ], function (angular, _, kbn) { 'use strict'; var module = angular.module('grafana.services'); - module.factory('OpenTSDBDatasource', function($q, $http) { + module.factory('OpenTSDBDatasource', function($q, $http, templateSrv) { function OpenTSDBDatasource(datasource) { this.type = 'opentsdb'; @@ -123,12 +124,12 @@ function (angular, _, kbn) { } var query = { - metric: target.metric, + metric: templateSrv.replace(target.metric), aggregator: "avg" }; if (target.aggregator) { - query.aggregator = target.aggregator; + query.aggregator = templateSrv.replace(target.aggregator); } if (target.shouldComputeRate) { @@ -136,6 +137,14 @@ function (angular, _, kbn) { query.rateOptions = { counter: !!target.isCounter }; + + if (target.counterMax && target.counterMax.length) { + query.rateOptions.counterMax = parseInt(target.counterMax); + } + + if (target.counterResetValue && target.counterResetValue.length) { + query.rateOptions.resetValue = parseInt(target.counterResetValue); + } } if (target.shouldDownsample) { @@ -143,6 +152,11 @@ function (angular, _, kbn) { } query.tags = angular.copy(target.tags); + if(query.tags){ + for(var key in query.tags){ + query.tags[key] = templateSrv.replace(query.tags[key]); + } + } return query; } diff --git a/src/css/less/grafana.less b/src/css/less/grafana.less index e88780a5752..7b534bcce40 100644 --- a/src/css/less/grafana.less +++ b/src/css/less/grafana.less @@ -16,7 +16,7 @@ .hide-controls { padding: 0; - .row-control-inner { + .row-tab { display: none; } .submenu-controls { diff --git a/src/css/less/graph.less b/src/css/less/graph.less index c0c3f371325..0da3014410f 100644 --- a/src/css/less/graph.less +++ b/src/css/less/graph.less @@ -178,9 +178,18 @@ top: -3px; } + .graph-tooltip-list-item { + display: table-row; + } + + .graph-tooltip-series-name { + display: table-cell; + } + .graph-tooltip-value { + display: table-cell; font-weight: bold; - float: right; padding-left: 10px; + text-align: right; } } diff --git a/src/plugins/datasource.example.js b/src/plugins/datasource.example.js index d85409cf2a5..fcc160ec569 100644 --- a/src/plugins/datasource.example.js +++ b/src/plugins/datasource.example.js @@ -19,7 +19,7 @@ function (angular, _, kbn) { this.url = datasource.url; } - CustomDatasource.prototype.query = function(filterSrv, options) { + CustomDatasource.prototype.query = function(options) { // get from & to in seconds var from = kbn.parseDate(options.range.from).getTime() / 1000; var to = kbn.parseDate(options.range.to).getTime() / 1000; diff --git a/src/test/specs/graph-ctrl-specs.js b/src/test/specs/graph-ctrl-specs.js index e1ddcbc571e..b2cf60d5d5d 100644 --- a/src/test/specs/graph-ctrl-specs.js +++ b/src/test/specs/graph-ctrl-specs.js @@ -36,6 +36,20 @@ define([ var data = ctx.scope.render.getCall(0).args[0]; expect(data.length).to.be(2); }); + + describe('get_data failure following success', function() { + beforeEach(function() { + ctx.datasource.query = sinon.stub().returns(ctx.$q.reject('Datasource Error')); + ctx.scope.get_data(); + ctx.scope.$digest(); + }); + + it('should clear the legend data', function() { + expect(ctx.scope.legend).to.eql([]); + }); + + }); + }); }); diff --git a/src/test/specs/influxQueryBuilder-specs.js b/src/test/specs/influxQueryBuilder-specs.js index 4f18bf2905e..b4e052a23af 100644 --- a/src/test/specs/influxQueryBuilder-specs.js +++ b/src/test/specs/influxQueryBuilder-specs.js @@ -44,6 +44,35 @@ define([ }); + describe('merge function detection', function() { + it('should not quote wrap regex merged series', function() { + var builder = new InfluxQueryBuilder({ + series: 'merge(/^google.test/)', + column: 'value', + function: 'mean' + }); + + var query = builder.build(); + + expect(query).to.be('select mean(value) from merge(/^google.test/) where $timeFilter ' + + 'group by time($interval) order asc'); + }); + + it('should quote wrap series names that start with "merge"', function() { + var builder = new InfluxQueryBuilder({ + series: 'merge.google.test', + column: 'value', + function: 'mean' + }); + + var query = builder.build(); + + expect(query).to.be('select mean(value) from "merge.google.test" where $timeFilter ' + + 'group by time($interval) order asc'); + }); + + }); + }); }); diff --git a/tasks/options/ngmin.js b/tasks/options/ngmin.js index 39dbb7a540a..82e7dee4f8d 100644 --- a/tasks/options/ngmin.js +++ b/tasks/options/ngmin.js @@ -10,6 +10,7 @@ module.exports = function(config) { 'app/filters/**/*.js', 'app/panels/**/*.js', 'app/routes/**/*.js', + 'plugins/**/*.js', 'app/app.js', 'vendor/angular/**/*.js', ],