From 11c8e80ea9f0e4474507b9b7aa912aba25362161 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Fri, 15 May 2015 11:38:22 +0200 Subject: [PATCH 01/89] Working on new query editor for influxdb 0.9, #1525 --- public/app/directives/all.js | 2 +- .../{graphiteSegment.js => metric.segment.js} | 13 +- .../graphite/partials/query.editor.html | 4 +- .../plugins/datasource/graphite/queryCtrl.js | 17 +- .../plugins/datasource/influxdb/funcEditor.js | 2 +- .../influxdb/partials/query.editor.html | 59 +++++-- .../plugins/datasource/influxdb/queryCtrl.js | 147 +++++++----------- public/css/less/grafana.less | 7 + public/test/specs/graphiteTargetCtrl-specs.js | 6 +- 9 files changed, 132 insertions(+), 125 deletions(-) rename public/app/directives/{graphiteSegment.js => metric.segment.js} (92%) diff --git a/public/app/directives/all.js b/public/app/directives/all.js index 4ab92d111d6..cd294c3df25 100644 --- a/public/app/directives/all.js +++ b/public/app/directives/all.js @@ -12,7 +12,7 @@ define([ './bootstrap-tagsinput', './bodyClass', './variableValueSelect', - './graphiteSegment', + './metric.segment', './grafanaVersionCheck', './dropdown.typeahead', './topnav', diff --git a/public/app/directives/graphiteSegment.js b/public/app/directives/metric.segment.js similarity index 92% rename from public/app/directives/graphiteSegment.js rename to public/app/directives/metric.segment.js index c8ad131e6c7..5510dea6657 100644 --- a/public/app/directives/graphiteSegment.js +++ b/public/app/directives/metric.segment.js @@ -9,7 +9,7 @@ function (angular, app, _, $) { angular .module('grafana.directives') - .directive('graphiteSegment', function($compile, $sce) { + .directive('metricSegment', function($compile, $sce) { var inputTemplate = ''; @@ -17,6 +17,12 @@ function (angular, app, _, $) { var buttonTemplate = ''; return { + scope: { + segment: "=", + getAltSegments: "&", + onValueChanged: "&" + }, + link: function($scope, elem) { var $input = $(inputTemplate); var $button = $(buttonTemplate); @@ -46,7 +52,7 @@ function (angular, app, _, $) { segment.expandable = true; segment.fake = false; } - $scope.segmentValueChanged(segment, $scope.$index); + $scope.onValueChanged(); }); }; @@ -69,7 +75,8 @@ function (angular, app, _, $) { if (options) { return options; } $scope.$apply(function() { - $scope.getAltSegments($scope.$index).then(function() { + $scope.getAltSegments().then(function(altSegments) { + $scope.altSegments = altSegments; options = _.map($scope.altSegments, function(alt) { return alt.value; }); // add custom values diff --git a/public/app/plugins/datasource/graphite/partials/query.editor.html b/public/app/plugins/datasource/graphite/partials/query.editor.html index 48446049e65..dcbbdc69a8a 100755 --- a/public/app/plugins/datasource/graphite/partials/query.editor.html +++ b/public/app/plugins/datasource/graphite/partials/query.editor.html @@ -74,7 +74,9 @@ ng-show="showTextEditor" /> - + +
diff --git a/public/app/plugins/datasource/influxdb/queryCtrl.js b/public/app/plugins/datasource/influxdb/queryCtrl.js index 00e12ea05bd..e78c100b5c5 100644 --- a/public/app/plugins/datasource/influxdb/queryCtrl.js +++ b/public/app/plugins/datasource/influxdb/queryCtrl.js @@ -7,19 +7,39 @@ function (angular, _) { var module = angular.module('grafana.controllers'); - module.controller('InfluxQueryCtrl', function($scope, $timeout, $sce, templateSrv, $q) { + module.controller('InfluxQueryCtrl', function($scope, $timeout, $sce, templateSrv) { + + $scope.functionList = [ + 'count', 'mean', 'sum', 'min', + 'max', 'mode', 'distinct', 'median', + 'derivative', 'stddev', 'first', 'last', + 'difference' + ]; + + $scope.functionMenu = _.map($scope.functionList, function(func) { + return { text: func, click: "changeFunction('" + func + "');" }; + }); $scope.init = function() { - $scope.segments = $scope.target.segments || []; + var target = $scope.target; + target.function = target.function || 'mean'; - $scope.functionsSelect = [ - 'count', 'mean', 'sum', 'min', - 'max', 'mode', 'distinct', 'median', - 'derivative', 'stddev', 'first', 'last', - 'difference' - ]; + if (!target.measurement) { + $scope.measurementSegment = MetricSegment.newSelectMeasurement(); + } else { + $scope.measurementSegment = new MetricSegment(target.measurement); + } + }; - checkOtherSegments(0); + $scope.changeFunction = function(func) { + $scope.target.function = func; + $scope.$parent.get_data(); + }; + + $scope.measurementChanged = function() { + $scope.target.measurement = $scope.measurementSegment.value; + console.log('measurement updated', $scope.target.measurement); + $scope.$parent.get_data(); }; $scope.toggleQueryMode = function () { @@ -35,103 +55,44 @@ function (angular, _) { $scope.panel.targets.push(clone); }; - $scope.getAltSegments = function (index) { - $scope.altSegments = []; - - var measurement = $scope.segments[0].value; - var queryType, query; - if (index === 0) { - queryType = 'MEASUREMENTS'; - query = 'SHOW MEASUREMENTS'; - } else if (index % 2 === 1) { - queryType = 'TAG_KEYS'; - query = 'SHOW TAG KEYS FROM "' + measurement + '"'; - } else { - queryType = 'TAG_VALUES'; - query = 'SHOW TAG VALUES FROM "' + measurement + '" WITH KEY = ' + $scope.segments[$scope.segments.length - 2].value; - } - - console.log('getAltSegments: query' , query); - - return $scope.datasource.metricFindQuery(query, queryType).then(function(results) { + $scope.getMeasurements = function () { + // var measurement = $scope.segments[0].value; + // var queryType, query; + // if (index === 0) { + // queryType = 'MEASUREMENTS'; + // query = 'SHOW MEASUREMENTS'; + // } else if (index % 2 === 1) { + // queryType = 'TAG_KEYS'; + // query = 'SHOW TAG KEYS FROM "' + measurement + '"'; + // } else { + // queryType = 'TAG_VALUES'; + // query = 'SHOW TAG VALUES FROM "' + measurement + '" WITH KEY = ' + $scope.segments[$scope.segments.length - 2].value; + // } + // + // console.log('getAltSegments: query' , query); + // + console.log('get measurements'); + return $scope.datasource.metricFindQuery('SHOW MEASUREMENTS', 'MEASUREMENTS').then(function(results) { console.log('get alt segments: response', results); - $scope.altSegments = _.map(results, function(segment) { + var measurements = _.map(results, function(segment) { return new MetricSegment({ value: segment.text, expandable: segment.expandable }); }); _.each(templateSrv.variables, function(variable) { - $scope.altSegments.unshift(new MetricSegment({ + measurements.unshift(new MetricSegment({ type: 'template', value: '$' + variable.name, expandable: true, })); }); + + return measurements; }, function(err) { $scope.parserError = err.message || 'Failed to issue metric query'; + return []; }); }; - $scope.segmentValueChanged = function (segment, segmentIndex) { - delete $scope.parserError; - - if (segment.expandable) { - return checkOtherSegments(segmentIndex + 1).then(function () { - setSegmentFocus(segmentIndex + 1); - $scope.targetChanged(); - }); - } - else { - $scope.segments = $scope.segments.splice(0, segmentIndex + 1); - } - - setSegmentFocus(segmentIndex + 1); - $scope.targetChanged(); - }; - - $scope.targetChanged = function() { - if ($scope.parserError) { - return; - } - - $scope.target.measurement = ''; - $scope.target.tags = {}; - $scope.target.measurement = $scope.segments[0].value; - - for (var i = 1; i+1 < $scope.segments.length; i += 2) { - var key = $scope.segments[i].value; - $scope.target.tags[key] = $scope.segments[i+1].value; - } - - $scope.$parent.get_data(); - }; - - function checkOtherSegments(fromIndex) { - if (fromIndex === 0) { - $scope.segments.push(MetricSegment.newSelectMetric()); - return; - } - - if ($scope.segments.length === 0) { - throw('should always have a scope segment?'); - } - - if (_.last($scope.segments).fake) { - return $q.when([]); - } else if ($scope.segments.length % 2 === 1) { - $scope.segments.push(MetricSegment.newSelectTag()); - return $q.when([]); - } else { - $scope.segments.push(MetricSegment.newSelectTagValue()); - return $q.when([]); - } - } - - function setSegmentFocus(segmentIndex) { - _.each($scope.segments, function(segment, index) { - segment.focus = segmentIndex === index; - }); - } - function MetricSegment(options) { if (options === '*' || options.value === '*') { this.value = '*'; @@ -153,8 +114,8 @@ function (angular, _) { this.html = $sce.trustAsHtml(templateSrv.highlightVariablesAsHtml(this.value)); } - MetricSegment.newSelectMetric = function() { - return new MetricSegment({value: 'select metric', fake: true}); + MetricSegment.newSelectMeasurement = function() { + return new MetricSegment({value: 'select measurement', fake: true}); }; MetricSegment.newSelectTag = function() { diff --git a/public/css/less/grafana.less b/public/css/less/grafana.less index 9efdf3dd360..9be157f80d0 100644 --- a/public/css/less/grafana.less +++ b/public/css/less/grafana.less @@ -337,3 +337,10 @@ text-overflow: ellipsis; } } + +.query-keyword { + font-weight: bold; + color: @blue; +} + + diff --git a/public/test/specs/graphiteTargetCtrl-specs.js b/public/test/specs/graphiteTargetCtrl-specs.js index a5217376098..31916cc2802 100644 --- a/public/test/specs/graphiteTargetCtrl-specs.js +++ b/public/test/specs/graphiteTargetCtrl-specs.js @@ -141,13 +141,15 @@ define([ ctx.scope.target.target = 'test.count'; ctx.scope.datasource.metricFindQuery.returns(ctx.$q.when([])); ctx.scope.init(); - ctx.scope.getAltSegments(1); + ctx.scope.getAltSegments(1).then(function(results) { + ctx.altSegments = results; + }); ctx.scope.$digest(); ctx.scope.$parent = { get_data: sinon.spy() }; }); it('should have no segments', function() { - expect(ctx.scope.altSegments.length).to.be(0); + expect(ctx.altSegments.length).to.be(0); }); }); From 5ca8d590bd9a769397afc24de5529f8b1c9ad429 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Fri, 15 May 2015 15:58:07 +0200 Subject: [PATCH 02/89] Working on new query editor for influxdb 0.9, looking good! #1525 --- public/app/directives/metric.segment.js | 3 +- .../influxdb/partials/query.editor.html | 13 +- .../datasource/influxdb/queryBuilder.js | 4 +- .../plugins/datasource/influxdb/queryCtrl.js | 177 ++++++++++++++---- .../test/specs/influx09-querybuilder-specs.js | 2 +- public/test/specs/influxdbQueryCtrl-specs.js | 120 ++++++++++++ public/test/test-main.js | 1 + 7 files changed, 270 insertions(+), 50 deletions(-) create mode 100644 public/test/specs/influxdbQueryCtrl-specs.js diff --git a/public/app/directives/metric.segment.js b/public/app/directives/metric.segment.js index 5510dea6657..05bf7e485e3 100644 --- a/public/app/directives/metric.segment.js +++ b/public/app/directives/metric.segment.js @@ -14,7 +14,8 @@ function (angular, app, _, $) { ' class="tight-form-clear-input input-medium"' + ' spellcheck="false" style="display:none">'; - var buttonTemplate = ''; + var buttonTemplate = ''; return { scope: { diff --git a/public/app/plugins/datasource/influxdb/partials/query.editor.html b/public/app/plugins/datasource/influxdb/partials/query.editor.html index 7210cfc24dc..494eaff5520 100644 --- a/public/app/plugins/datasource/influxdb/partials/query.editor.html +++ b/public/app/plugins/datasource/influxdb/partials/query.editor.html @@ -83,15 +83,16 @@
  • WHERE
  • -
  • - +
  • +
  • GROUP BY - time($interval), + time($interval) +
  • +
  • +
  • -
  • fill (null)
  • fill (0)
  • - + diff --git a/public/app/plugins/datasource/influxdb/queryBuilder.js b/public/app/plugins/datasource/influxdb/queryBuilder.js index e8eaefda61a..9317cea4574 100644 --- a/public/app/plugins/datasource/influxdb/queryBuilder.js +++ b/public/app/plugins/datasource/influxdb/queryBuilder.js @@ -33,8 +33,8 @@ function (_) { query += aggregationFunc + '(value)'; query += ' FROM ' + measurement + ' WHERE $timeFilter'; - query += _.map(target.tags, function(value, key) { - return ' AND ' + key + '=' + "'" + value + "'"; + query += _.map(target.tags, function(tag) { + return ' AND ' + tag.key + '=' + "'" + tag.value + "'"; }).join(''); query += ' GROUP BY time($interval)'; diff --git a/public/app/plugins/datasource/influxdb/queryCtrl.js b/public/app/plugins/datasource/influxdb/queryCtrl.js index e78c100b5c5..5aeb68f00e7 100644 --- a/public/app/plugins/datasource/influxdb/queryCtrl.js +++ b/public/app/plugins/datasource/influxdb/queryCtrl.js @@ -7,13 +7,11 @@ function (angular, _) { var module = angular.module('grafana.controllers'); - module.controller('InfluxQueryCtrl', function($scope, $timeout, $sce, templateSrv) { + module.controller('InfluxQueryCtrl', function($scope, $timeout, $sce, templateSrv, $q) { $scope.functionList = [ - 'count', 'mean', 'sum', 'min', - 'max', 'mode', 'distinct', 'median', - 'derivative', 'stddev', 'first', 'last', - 'difference' + 'count', 'mean', 'sum', 'min', 'max', 'mode', 'distinct', 'median', + 'derivative', 'stddev', 'first', 'last', 'difference' ]; $scope.functionMenu = _.map($scope.functionList, function(func) { @@ -23,12 +21,41 @@ function (angular, _) { $scope.init = function() { var target = $scope.target; target.function = target.function || 'mean'; + target.tags = target.tags || []; + target.groupByTags = target.groupByTags || []; if (!target.measurement) { $scope.measurementSegment = MetricSegment.newSelectMeasurement(); } else { $scope.measurementSegment = new MetricSegment(target.measurement); } + + $scope.tagSegments = []; + _.each(target.tags, function(tag) { + if (tag.condition) { + $scope.tagSegments.push(MetricSegment.newCondition(tag.condition)); + } + $scope.tagSegments.push(new MetricSegment({value: tag.key, type: 'key' })); + $scope.tagSegments.push(new MetricSegment({fake: true, value: "="})); + $scope.tagSegments.push(new MetricSegment({value: tag.value, type: 'value'})); + }); + + if ($scope.tagSegments.length % 3 === 0) { + $scope.tagSegments.push(MetricSegment.newPlusButton()); + } + + $scope.groupBySegments = []; + _.each(target.groupByTags, function(tag) { + $scope.groupBySegments.push(new MetricSegment(tag)); + }); + + $scope.groupBySegments.push(MetricSegment.newPlusButton()); + }; + + $scope.groupByTagUpdated = function(segment, index) { + if (index === $scope.groupBySegments.length-1) { + $scope.groupBySegments.push(MetricSegment.newPlusButton()); + } }; $scope.changeFunction = function(func) { @@ -56,43 +83,107 @@ function (angular, _) { }; $scope.getMeasurements = function () { - // var measurement = $scope.segments[0].value; - // var queryType, query; - // if (index === 0) { - // queryType = 'MEASUREMENTS'; - // query = 'SHOW MEASUREMENTS'; - // } else if (index % 2 === 1) { - // queryType = 'TAG_KEYS'; - // query = 'SHOW TAG KEYS FROM "' + measurement + '"'; - // } else { - // queryType = 'TAG_VALUES'; - // query = 'SHOW TAG VALUES FROM "' + measurement + '" WITH KEY = ' + $scope.segments[$scope.segments.length - 2].value; - // } - // - // console.log('getAltSegments: query' , query); - // - console.log('get measurements'); - return $scope.datasource.metricFindQuery('SHOW MEASUREMENTS', 'MEASUREMENTS').then(function(results) { - console.log('get alt segments: response', results); - var measurements = _.map(results, function(segment) { - return new MetricSegment({ value: segment.text, expandable: segment.expandable }); - }); + return $scope.datasource.metricFindQuery('SHOW MEASUREMENTS', 'MEASUREMENTS') + .then($scope.transformToSegments) + .then($scope.addTemplateVariableSegments) + .then(null, $scope.handleQueryError); + }; - _.each(templateSrv.variables, function(variable) { - measurements.unshift(new MetricSegment({ - type: 'template', - value: '$' + variable.name, - expandable: true, - })); - }); + $scope.handleQueryError = function(err) { + $scope.parserError = err.message || 'Failed to issue metric query'; + return []; + }; - return measurements; - }, function(err) { - $scope.parserError = err.message || 'Failed to issue metric query'; - return []; + $scope.transformToSegments = function(results) { + return _.map(results, function(segment) { + return new MetricSegment({ value: segment.text, expandable: segment.expandable }); }); }; + $scope.addTemplateVariableSegments = function(segments) { + _.each(templateSrv.variables, function(variable) { + segments.unshift(new MetricSegment({ type: 'template', value: '$' + variable.name, expandable: true })); + }); + return segments; + }; + + $scope.getTagsOrValues = function(segment, index) { + var query, queryType; + if (segment.type === 'key' || segment.type === 'plus-button') { + queryType = 'TAG_KEYS'; + query = 'SHOW TAG KEYS FROM "' + $scope.target.measurement + '"'; + } else if (segment.type === 'value') { + queryType = 'TAG_VALUES'; + query = 'SHOW TAG VALUES FROM "' + $scope.target.measurement + '" WITH KEY = ' + $scope.tagSegments[index-2].value; + } else if (segment.type === 'condition') { + return $q.when([new MetricSegment('AND'), new MetricSegment('OR')]); + } + else { + return $q.when([]); + } + + return $scope.datasource.metricFindQuery(query, queryType) + .then($scope.transformToSegments) + .then($scope.addTemplateVariableSegments) + .then(function(results) { + if (queryType === 'TAG_KEYS' && segment.type !== 'plus-button') { + results.push(new MetricSegment({fake: true, value: 'remove tag filter'})); + } + return results; + }) + .then(null, $scope.handleQueryError); + }; + + $scope.tagSegmentUpdated = function(segment, index) { + $scope.tagSegments[index] = segment; + + if (segment.value === 'remove tag filter') { + $scope.tagSegments.splice(index, 3); + if ($scope.tagSegments.length === 0) { + $scope.tagSegments.push(MetricSegment.newPlusButton()); + } else { + $scope.tagSegments.splice(index-1, 1); + $scope.tagSegments.push(MetricSegment.newPlusButton()); + } + } + else { + if (segment.type === 'plus-button') { + if (index > 2) { + $scope.tagSegments.splice(index, 0, MetricSegment.newCondition('AND')); + } + $scope.tagSegments.push(new MetricSegment({fake: true, value: '=', type: 'operator'})); + $scope.tagSegments.push(new MetricSegment({fake: true, value: 'select tag value', type: 'value' })); + segment.type = 'key'; + } + + if ((index+1) === $scope.tagSegments.length) { + $scope.tagSegments.push(MetricSegment.newPlusButton()); + } + } + + $scope.rebuildTargetTagConditions(); + }; + + $scope.rebuildTargetTagConditions = function() { + var tags = [{}]; + var tagIndex = 0; + _.each($scope.tagSegments, function(segment2) { + if (segment2.type === 'key') { + tags[tagIndex].key = segment2.value; + } + else if (segment2.type === 'value') { + tags[tagIndex].value = segment2.value; + } + else if (segment2.type === 'condition') { + tags.push({ condition: segment2.value }); + tagIndex += 1; + } + }); + + $scope.target.tags = tags; + $scope.$parent.get_data(); + }; + function MetricSegment(options) { if (options === '*' || options.value === '*') { this.value = '*'; @@ -107,19 +198,25 @@ function (angular, _) { return; } + this.cssClass = options.cssClass; + this.type = options.type; this.fake = options.fake; this.value = options.value; this.type = options.type; this.expandable = options.expandable; - this.html = $sce.trustAsHtml(templateSrv.highlightVariablesAsHtml(this.value)); + this.html = options.html || $sce.trustAsHtml(templateSrv.highlightVariablesAsHtml(this.value)); } MetricSegment.newSelectMeasurement = function() { return new MetricSegment({value: 'select measurement', fake: true}); }; - MetricSegment.newSelectTag = function() { - return new MetricSegment({value: 'select tag', fake: true}); + MetricSegment.newCondition = function(condition) { + return new MetricSegment({value: condition, type: 'condition', cssClass: 'query-keyword' }); + }; + + MetricSegment.newPlusButton = function() { + return new MetricSegment({fake: true, html: '', type: 'plus-button' }); }; MetricSegment.newSelectTagValue = function() { diff --git a/public/test/specs/influx09-querybuilder-specs.js b/public/test/specs/influx09-querybuilder-specs.js index 09fb343171a..bec2035760f 100644 --- a/public/test/specs/influx09-querybuilder-specs.js +++ b/public/test/specs/influx09-querybuilder-specs.js @@ -21,7 +21,7 @@ define([ describe('series with tags only', function() { var builder = new InfluxQueryBuilder({ measurement: 'cpu', - tags: {'hostname': 'server1'} + tags: [{key: 'hostname', value: 'server1'}] }); var query = builder.build(); diff --git a/public/test/specs/influxdbQueryCtrl-specs.js b/public/test/specs/influxdbQueryCtrl-specs.js new file mode 100644 index 00000000000..0e177a6aebe --- /dev/null +++ b/public/test/specs/influxdbQueryCtrl-specs.js @@ -0,0 +1,120 @@ +define([ + 'helpers', + 'plugins/datasource/influxdb/queryCtrl' +], function(helpers) { + 'use strict'; + + describe('InfluxDBQueryCtrl', function() { + var ctx = new helpers.ControllerTestContext(); + + beforeEach(module('grafana.controllers')); + beforeEach(ctx.providePhase()); + beforeEach(ctx.createControllerPhase('InfluxQueryCtrl')); + + beforeEach(function() { + ctx.scope.target = {}; + ctx.scope.$parent = { get_data: sinon.spy() }; + + ctx.scope.datasource = ctx.datasource; + ctx.scope.datasource.metricFindQuery = sinon.stub().returns(ctx.$q.when([])); + }); + + describe('init', function() { + beforeEach(function() { + ctx.scope.init(); + }); + + it('should init tagSegments', function() { + expect(ctx.scope.tagSegments.length).to.be(1); + }); + + it('should init measurementSegment', function() { + expect(ctx.scope.measurementSegment.value).to.be('select measurement'); + }); + }); + + describe('when first tag segment is updated', function() { + beforeEach(function() { + ctx.scope.init(); + ctx.scope.tagSegmentUpdated({value: 'asd', type: 'plus-button'}, 0); + }); + + it('should update tag key', function() { + expect(ctx.scope.target.tags[0].key).to.be('asd'); + expect(ctx.scope.tagSegments[0].type).to.be('key'); + }); + + it('should add tagSegments', function() { + expect(ctx.scope.tagSegments.length).to.be(3); + }); + }); + + describe('when last tag value segment is updated', function() { + beforeEach(function() { + ctx.scope.init(); + ctx.scope.tagSegmentUpdated({value: 'asd', type: 'plus-button'}, 0); + ctx.scope.tagSegmentUpdated({value: 'server1', type: 'value'}, 2); + }); + + it('should update tag value', function() { + expect(ctx.scope.target.tags[0].value).to.be('server1'); + }); + + it('should add plus button for another filter', function() { + expect(ctx.scope.tagSegments[3].fake).to.be(true); + }); + }); + + describe('when second tag key is added', function() { + beforeEach(function() { + ctx.scope.init(); + ctx.scope.tagSegmentUpdated({value: 'asd', type: 'plus-button' }, 0); + ctx.scope.tagSegmentUpdated({value: 'server1', type: 'value'}, 2); + ctx.scope.tagSegmentUpdated({value: 'key2', type: 'plus-button'}, 3); + }); + + it('should update tag key', function() { + expect(ctx.scope.target.tags[1].key).to.be('key2'); + }); + + it('should add AND segment', function() { + expect(ctx.scope.tagSegments[3].value).to.be('AND'); + }); + }); + + describe('when condition is changed', function() { + beforeEach(function() { + ctx.scope.init(); + ctx.scope.tagSegmentUpdated({value: 'asd', type: 'plus-button' }, 0); + ctx.scope.tagSegmentUpdated({value: 'server1', type: 'value'}, 2); + ctx.scope.tagSegmentUpdated({value: 'key2', type: 'plus-button'}, 3); + ctx.scope.tagSegmentUpdated({value: 'OR', type: 'condition'}, 3); + }); + + it('should update tag condition', function() { + expect(ctx.scope.target.tags[1].condition).to.be('OR'); + }); + + it('should update AND segment', function() { + expect(ctx.scope.tagSegments[3].value).to.be('OR'); + expect(ctx.scope.tagSegments.length).to.be(7); + }); + }); + + describe('when deleting is changed', function() { + beforeEach(function() { + ctx.scope.init(); + ctx.scope.tagSegmentUpdated({value: 'asd', type: 'plus-button' }, 0); + ctx.scope.tagSegmentUpdated({value: 'server1', type: 'value'}, 2); + ctx.scope.tagSegmentUpdated({value: 'key2', type: 'plus-button'}, 3); + ctx.scope.tagSegmentUpdated({value: 'remove tag filter', type: 'key'}, 4); + }); + + it('should remove all segment after 2 and replace with plus button', function() { + expect(ctx.scope.tagSegments.length).to.be(4); + expect(ctx.scope.tagSegments[3].type).to.be('plus-button'); + }); + }); + + }); +}); diff --git a/public/test/test-main.js b/public/test/test-main.js index 3e944c3c36f..86196069265 100644 --- a/public/test/test-main.js +++ b/public/test/test-main.js @@ -128,6 +128,7 @@ require([ 'specs/influxQueryBuilder-specs', 'specs/influx09-querybuilder-specs', 'specs/influxdb-datasource-specs', + 'specs/influxdbQueryCtrl-specs', 'specs/graph-ctrl-specs', 'specs/graph-specs', 'specs/graph-tooltip-specs', From 09b0e6e388dc802b090ce016838bf0ef4ccb4874 Mon Sep 17 00:00:00 2001 From: "Haneysmith, Nathan" Date: Fri, 15 May 2015 11:11:02 -0700 Subject: [PATCH 03/89] Addresses #1853, redact session provider secrets In cases where a database is used for session storage, redact the session_provider config value. I assumed "@" as the marker for a database vs file/memory. --- pkg/api/admin_settings.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/api/admin_settings.go b/pkg/api/admin_settings.go index 21615219acd..71ed229bba7 100644 --- a/pkg/api/admin_settings.go +++ b/pkg/api/admin_settings.go @@ -17,7 +17,7 @@ func AdminGetSettings(c *middleware.Context) { for _, key := range section.Keys() { keyName := key.Name() value := key.Value() - if strings.Contains(keyName, "secret") || strings.Contains(keyName, "password") { + if strings.Contains(keyName, "secret") || strings.Contains(keyName, "password") || (strings.Contains(keyName, "provider_config") && strings.Contains(value, "@")) { value = "************" } From 2af28b90c2a3e9c4fbd5280af90eae490bf1b591 Mon Sep 17 00:00:00 2001 From: "Haneysmith, Nathan" Date: Fri, 15 May 2015 13:25:41 -0700 Subject: [PATCH 04/89] whitespace update per gofmt --- pkg/api/admin_settings.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/api/admin_settings.go b/pkg/api/admin_settings.go index 71ed229bba7..06413d6a0b1 100644 --- a/pkg/api/admin_settings.go +++ b/pkg/api/admin_settings.go @@ -17,7 +17,7 @@ func AdminGetSettings(c *middleware.Context) { for _, key := range section.Keys() { keyName := key.Name() value := key.Value() - if strings.Contains(keyName, "secret") || strings.Contains(keyName, "password") || (strings.Contains(keyName, "provider_config") && strings.Contains(value, "@")) { + if strings.Contains(keyName, "secret") || strings.Contains(keyName, "password") || (strings.Contains(keyName, "provider_config") && strings.Contains(value, "@")) { value = "************" } From 6fd37779b8538981dde8fe9d147d285819930209 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Fri, 15 May 2015 19:04:49 +0200 Subject: [PATCH 05/89] More work on new influxdb query editor, #1525 --- .../influxdb/partials/query.editor.html | 2 +- .../datasource/influxdb/queryBuilder.js | 16 ++-- .../plugins/datasource/influxdb/queryCtrl.js | 79 +++++++++++++---- public/css/less/grafana.less | 12 +++ .../test/specs/influx09-querybuilder-specs.js | 35 +++++++- public/test/specs/influxdbQueryCtrl-specs.js | 86 ++++++++++++++++++- 6 files changed, 203 insertions(+), 27 deletions(-) diff --git a/public/app/plugins/datasource/influxdb/partials/query.editor.html b/public/app/plugins/datasource/influxdb/partials/query.editor.html index 494eaff5520..8ea612b0924 100644 --- a/public/app/plugins/datasource/influxdb/partials/query.editor.html +++ b/public/app/plugins/datasource/influxdb/partials/query.editor.html @@ -91,7 +91,7 @@ time($interval)
  • - +
  • - +
  • Type @@ -139,7 +139,7 @@ Query
  • - +
  • @@ -151,7 +151,7 @@ Optional, if you want to extract part of a series name or metric node segment
  • - +
  • @@ -163,7 +163,7 @@
  • - +
  • All format From 5896903bd37739ca4b04186cc18a1982ad27e1e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 18 May 2015 15:01:05 +0200 Subject: [PATCH 13/89] Began work on alias support and alias patterns for InfluxDB 0.9, #1525 --- .../plugins/datasource/influxdb/datasource.js | 15 +- .../datasource/influxdb/influxSeries.js | 19 +- .../influxdb/partials/query.editor.html | 89 +++++++ public/css/less/tightform.less | 2 +- public/test/specs/influxSeries-specs.js | 245 +++--------------- public/test/specs/influxSeries08-specs.js | 220 ++++++++++++++++ public/test/test-main.js | 1 + 7 files changed, 373 insertions(+), 218 deletions(-) create mode 100644 public/test/specs/influxSeries08-specs.js diff --git a/public/app/plugins/datasource/influxdb/datasource.js b/public/app/plugins/datasource/influxdb/datasource.js index 56b01772c55..de7379d6367 100644 --- a/public/app/plugins/datasource/influxdb/datasource.js +++ b/public/app/plugins/datasource/influxdb/datasource.js @@ -69,8 +69,11 @@ function (angular, _, kbn, InfluxSeries, InfluxQueryBuilder) { var query = annotation.query.replace('$timeFilter', timeFilter); query = templateSrv.replace(query); - return this._seriesQuery(query).then(function(results) { - return new InfluxSeries({ seriesList: results, annotation: annotation }).getAnnotations(); + return this._seriesQuery(query).then(function(data) { + if (!data || !data.results || !data.results[0]) { + throw { message: 'No results in response from InfluxDB' }; + } + return new InfluxSeries({ series: data.results[0].series, annotation: annotation }).getAnnotations(); }); }; @@ -168,9 +171,11 @@ function (angular, _, kbn, InfluxSeries, InfluxQueryBuilder) { return deferred.promise; }; - function handleInfluxQueryResponse(alias, seriesList) { - var influxSeries = new InfluxSeries({ seriesList: seriesList, alias: alias }); - return influxSeries.getTimeSeries(); + function handleInfluxQueryResponse(alias, data) { + if (!data || !data.results || !data.results[0]) { + throw { message: 'No results in response from InfluxDB' }; + } + return new InfluxSeries({ series: data.results[0].series, alias: alias }).getTimeSeries(); } function getTimeFilter(options) { diff --git a/public/app/plugins/datasource/influxdb/influxSeries.js b/public/app/plugins/datasource/influxdb/influxSeries.js index cca01459fc4..45e4325c016 100644 --- a/public/app/plugins/datasource/influxdb/influxSeries.js +++ b/public/app/plugins/datasource/influxdb/influxSeries.js @@ -5,8 +5,7 @@ function (_) { 'use strict'; function InfluxSeries(options) { - this.seriesList = options.seriesList && options.seriesList.results && options.seriesList.results.length > 0 - ? options.seriesList.results[0].series || [] : []; + this.series = options.series; this.alias = options.alias; this.annotation = options.annotation; } @@ -17,23 +16,25 @@ function (_) { var output = []; var self = this; - console.log(self.seriesList); - if (self.seriesList.length === 0) { + if (self.series.length === 0) { return output; } - _.each(self.seriesList, function(series) { + _.each(self.series, function(series) { var datapoints = []; for (var i = 0; i < series.values.length; i++) { datapoints[i] = [series.values[i][1], new Date(series.values[i][0]).getTime()]; } var seriesName = series.name; - var tags = _.map(series.tags, function(value, key) { - return key + ': ' + value; - }); - if (tags.length > 0) { + if (self.alias) { + seriesName = self.alias; + } else if (series.tags) { + var tags = _.map(series.tags, function(value, key) { + return key + ': ' + value; + }); + seriesName = seriesName + ' {' + tags.join(', ') + '}'; } diff --git a/public/app/plugins/datasource/influxdb/partials/query.editor.html b/public/app/plugins/datasource/influxdb/partials/query.editor.html index 0881921078d..9a9fca802ea 100644 --- a/public/app/plugins/datasource/influxdb/partials/query.editor.html +++ b/public/app/plugins/datasource/influxdb/partials/query.editor.html @@ -141,14 +141,103 @@
  • +
      +
    • + Alias pattern +
    • +
    • + +
    • +
    +
    +
    +
      +
    • + +
    • +
    • + Group by time interval +
    • +
    • + +
    • +
    • + +
    • +
    +
    +
    + + +
    +
    + +
    +
    Alias patterns
    +
      +
    • $m = replaced with measurement name
    • +
    • $measurement = replaced with measurement name
    • +
    • $tag_hostname = replaced with the value of the hostname tag
    • +
    • You can also use [[tag_hostname]] pattern replacement syntax
    • +
    +
    + +
    +
    Stacking and fill
    +
      +
    • When stacking is enabled it important that points align
    • +
    • If there are missing points for one series it can cause gaps or missing bars
    • +
    • You must use fill(0), and select a group by time low limit
    • +
    • Use the group by time option below your queries and specify for example >10s if your metrics are written every 10 seconds
    • +
    • This will insert zeros for series that are missing measurements and will make stacking work properly
    • +
    +
    + +
    +
    Group by time
    +
      +
    • Group by time is important, otherwise the query could return many thousands of datapoints that will slow down Grafana
    • +
    • 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
    • +
    • If you use fill(0) or fill(null) set a low limit for the auto group by time interval
    • +
    • The low limit can only be set in the group by time option below your queries
    • +
    • You set a low limit by adding a greater sign before the interval
    • +
    • Example: >60s if you write metrics to InfluxDB every 60 seconds
    • +
    +
    + +
    + diff --git a/public/css/less/tightform.less b/public/css/less/tightform.less index 41630400cf7..5bd9cda8f43 100644 --- a/public/css/less/tightform.less +++ b/public/css/less/tightform.less @@ -23,7 +23,7 @@ .tight-form-container-no-item-borders { border: 1px solid @grafanaTargetBorder; - .tight-form, .tight-form-item { + .tight-form, .tight-form-item, [type=text].tight-form-input { border: none; } } diff --git a/public/test/specs/influxSeries-specs.js b/public/test/specs/influxSeries-specs.js index 47fb77b67b3..b68a3a95e8c 100644 --- a/public/test/specs/influxSeries-specs.js +++ b/public/test/specs/influxSeries-specs.js @@ -1,218 +1,57 @@ define([ - 'plugins/datasource/influxdb_08/influxSeries' + 'plugins/datasource/influxdb/influxSeries' ], function(InfluxSeries) { 'use strict'; describe('when generating timeseries from influxdb response', function() { describe('given two series', function() { - var series = new InfluxSeries({ - seriesList: [ - { - columns: ['time', 'mean', 'sequence_number'], - name: 'prod.server1.cpu', - points: [[1402596000, 10, 1], [1402596001, 12, 2]] - }, - { - columns: ['time', 'mean', 'sequence_number'], - name: 'prod.server2.cpu', - points: [[1402596000, 15, 1], [1402596001, 16, 2]] - } - ] - }); - - var result = series.getTimeSeries(); - - it('should generate two time series', function() { - expect(result.length).to.be(2); - expect(result[0].target).to.be('prod.server1.cpu.mean'); - expect(result[0].datapoints[0][0]).to.be(10); - expect(result[0].datapoints[0][1]).to.be(1402596000); - expect(result[0].datapoints[1][0]).to.be(12); - expect(result[0].datapoints[1][1]).to.be(1402596001); - - expect(result[1].target).to.be('prod.server2.cpu.mean'); - expect(result[1].datapoints[0][0]).to.be(15); - expect(result[1].datapoints[0][1]).to.be(1402596000); - expect(result[1].datapoints[1][0]).to.be(16); - expect(result[1].datapoints[1][1]).to.be(1402596001); - }); - - }); - - describe('given an alias format', function() { - var series = new InfluxSeries({ - seriesList: [ - { - columns: ['time', 'mean', 'sequence_number'], - name: 'prod.server1.cpu', - points: [[1402596000, 10, 1], [1402596001, 12, 2]] - } - ], - alias: '$s.testing' - }); - - var result = series.getTimeSeries(); - - it('should generate correct series name', function() { - expect(result[0].target).to.be('prod.server1.cpu.testing'); - }); - - }); - - describe('given an alias format with segment numbers', function() { - var series = new InfluxSeries({ - seriesList: [ - { - columns: ['time', 'mean', 'sequence_number'], - name: 'prod.server1.cpu', - points: [[1402596000, 10, 1], [1402596001, 12, 2]] - } - ], - alias: '$1.mean' - }); - - var result = series.getTimeSeries(); - - it('should generate correct series name', function() { - expect(result[0].target).to.be('server1.mean'); - }); - - }); - - describe('given an alias format and many segments', function() { - var series = new InfluxSeries({ - seriesList: [ - { - columns: ['time', 'mean', 'sequence_number'], - name: 'a0.a1.a2.a3.a4.a5.a6.a7.a8.a9.a10.a11.a12', - points: [[1402596000, 10, 1], [1402596001, 12, 2]] - } - ], - alias: '$5.$11.mean' - }); - - var result = series.getTimeSeries(); - - it('should generate correct series name', function() { - expect(result[0].target).to.be('a5.a11.mean'); - }); - - }); - - - describe('given an alias format with group by field', function() { - var series = new InfluxSeries({ - seriesList: [ - { - columns: ['time', 'mean', 'host'], - name: 'prod.cpu', - points: [[1402596000, 10, 'A']] - } - ], - groupByField: 'host', - alias: '$g.$1' - }); - - var result = series.getTimeSeries(); - - it('should generate correct series name', function() { - expect(result[0].target).to.be('A.cpu'); - }); - - }); - - describe('given group by column', function() { - var series = new InfluxSeries({ - seriesList: [ - { - columns: ['time', 'mean', 'host'], - name: 'prod.cpu', - points: [ - [1402596000, 10, 'A'], - [1402596001, 11, 'A'], - [1402596000, 5, 'B'], - [1402596001, 6, 'B'], - ] - } - ], - groupByField: 'host' - }); - - var result = series.getTimeSeries(); - - it('should generate two time series', function() { - expect(result.length).to.be(2); - expect(result[0].target).to.be('prod.cpu.A'); - expect(result[0].datapoints[0][0]).to.be(10); - expect(result[0].datapoints[0][1]).to.be(1402596000); - expect(result[0].datapoints[1][0]).to.be(11); - expect(result[0].datapoints[1][1]).to.be(1402596001); - - expect(result[1].target).to.be('prod.cpu.B'); - expect(result[1].datapoints[0][0]).to.be(5); - expect(result[1].datapoints[0][1]).to.be(1402596000); - expect(result[1].datapoints[1][0]).to.be(6); - expect(result[1].datapoints[1][1]).to.be(1402596001); - }); - - }); - - }); - - describe("when creating annotations from influxdb response", function() { - describe('given column mapping for all columns', function() { - var series = new InfluxSeries({ - seriesList: [ - { - columns: ['time', 'text', 'sequence_number', 'title', 'tags'], - name: 'events1', - points: [[1402596000000, 'some text', 1, 'Hello', 'B'], [1402596001000, 'asd', 2, 'Hello2', 'B']] - } - ], - annotation: { - query: 'select', - titleColumn: 'title', - tagsColumn: 'tags', - textColumn: 'text', + var options = { series: [ + { + name: 'cpu', + tags: {app: 'test'}, + columns: ['time', 'mean'], + values: [["2015-05-18T10:57:05Z", 10], ["2015-05-18T10:57:06Z", 12]] + }, + { + name: 'cpu', + tags: {app: 'test2'}, + columns: ['time', 'mean'], + values: [["2015-05-18T10:57:05Z", 15], ["2015-05-18T10:57:06Z", 16]] } + ]}; + + describe('and no alias', function() { + + it('should generate two time series', function() { + var series = new InfluxSeries(options); + var result = series.getTimeSeries(); + + expect(result.length).to.be(2); + expect(result[0].target).to.be('cpu {app: test}'); + expect(result[0].datapoints[0][0]).to.be(10); + expect(result[0].datapoints[0][1]).to.be(1431946625000); + expect(result[0].datapoints[1][0]).to.be(12); + expect(result[0].datapoints[1][1]).to.be(1431946626000); + + expect(result[1].target).to.be('cpu {app: test2}'); + expect(result[1].datapoints[0][0]).to.be(15); + expect(result[1].datapoints[0][1]).to.be(1431946625000); + expect(result[1].datapoints[1][0]).to.be(16); + expect(result[1].datapoints[1][1]).to.be(1431946626000); + }); }); - var result = series.getAnnotations(); + describe('and simple alias', function() { + it('should use alias', function() { + options.alias = 'new series'; + var series = new InfluxSeries(options); + var result = series.getTimeSeries(); + + expect(result[0].target).to.be('new series'); + }); - it(' should generate 2 annnotations ', function() { - expect(result.length).to.be(2); - expect(result[0].annotation.query).to.be('select'); - expect(result[0].title).to.be('Hello'); - expect(result[0].time).to.be(1402596000000); - expect(result[0].tags).to.be('B'); - expect(result[0].text).to.be('some text'); }); - - }); - - describe('given no column mapping', function() { - var series = new InfluxSeries({ - seriesList: [ - { - columns: ['time', 'text', 'sequence_number'], - name: 'events1', - points: [[1402596000000, 'some text', 1]] - } - ], - annotation: { query: 'select' } - }); - - var result = series.getAnnotations(); - - it('should generate 1 annnotation', function() { - expect(result.length).to.be(1); - expect(result[0].title).to.be('some text'); - expect(result[0].time).to.be(1402596000000); - expect(result[0].tags).to.be(undefined); - expect(result[0].text).to.be(undefined); - }); - }); }); diff --git a/public/test/specs/influxSeries08-specs.js b/public/test/specs/influxSeries08-specs.js new file mode 100644 index 00000000000..47fb77b67b3 --- /dev/null +++ b/public/test/specs/influxSeries08-specs.js @@ -0,0 +1,220 @@ +define([ + 'plugins/datasource/influxdb_08/influxSeries' +], function(InfluxSeries) { + 'use strict'; + + describe('when generating timeseries from influxdb response', function() { + + describe('given two series', function() { + var series = new InfluxSeries({ + seriesList: [ + { + columns: ['time', 'mean', 'sequence_number'], + name: 'prod.server1.cpu', + points: [[1402596000, 10, 1], [1402596001, 12, 2]] + }, + { + columns: ['time', 'mean', 'sequence_number'], + name: 'prod.server2.cpu', + points: [[1402596000, 15, 1], [1402596001, 16, 2]] + } + ] + }); + + var result = series.getTimeSeries(); + + it('should generate two time series', function() { + expect(result.length).to.be(2); + expect(result[0].target).to.be('prod.server1.cpu.mean'); + expect(result[0].datapoints[0][0]).to.be(10); + expect(result[0].datapoints[0][1]).to.be(1402596000); + expect(result[0].datapoints[1][0]).to.be(12); + expect(result[0].datapoints[1][1]).to.be(1402596001); + + expect(result[1].target).to.be('prod.server2.cpu.mean'); + expect(result[1].datapoints[0][0]).to.be(15); + expect(result[1].datapoints[0][1]).to.be(1402596000); + expect(result[1].datapoints[1][0]).to.be(16); + expect(result[1].datapoints[1][1]).to.be(1402596001); + }); + + }); + + describe('given an alias format', function() { + var series = new InfluxSeries({ + seriesList: [ + { + columns: ['time', 'mean', 'sequence_number'], + name: 'prod.server1.cpu', + points: [[1402596000, 10, 1], [1402596001, 12, 2]] + } + ], + alias: '$s.testing' + }); + + var result = series.getTimeSeries(); + + it('should generate correct series name', function() { + expect(result[0].target).to.be('prod.server1.cpu.testing'); + }); + + }); + + describe('given an alias format with segment numbers', function() { + var series = new InfluxSeries({ + seriesList: [ + { + columns: ['time', 'mean', 'sequence_number'], + name: 'prod.server1.cpu', + points: [[1402596000, 10, 1], [1402596001, 12, 2]] + } + ], + alias: '$1.mean' + }); + + var result = series.getTimeSeries(); + + it('should generate correct series name', function() { + expect(result[0].target).to.be('server1.mean'); + }); + + }); + + describe('given an alias format and many segments', function() { + var series = new InfluxSeries({ + seriesList: [ + { + columns: ['time', 'mean', 'sequence_number'], + name: 'a0.a1.a2.a3.a4.a5.a6.a7.a8.a9.a10.a11.a12', + points: [[1402596000, 10, 1], [1402596001, 12, 2]] + } + ], + alias: '$5.$11.mean' + }); + + var result = series.getTimeSeries(); + + it('should generate correct series name', function() { + expect(result[0].target).to.be('a5.a11.mean'); + }); + + }); + + + describe('given an alias format with group by field', function() { + var series = new InfluxSeries({ + seriesList: [ + { + columns: ['time', 'mean', 'host'], + name: 'prod.cpu', + points: [[1402596000, 10, 'A']] + } + ], + groupByField: 'host', + alias: '$g.$1' + }); + + var result = series.getTimeSeries(); + + it('should generate correct series name', function() { + expect(result[0].target).to.be('A.cpu'); + }); + + }); + + describe('given group by column', function() { + var series = new InfluxSeries({ + seriesList: [ + { + columns: ['time', 'mean', 'host'], + name: 'prod.cpu', + points: [ + [1402596000, 10, 'A'], + [1402596001, 11, 'A'], + [1402596000, 5, 'B'], + [1402596001, 6, 'B'], + ] + } + ], + groupByField: 'host' + }); + + var result = series.getTimeSeries(); + + it('should generate two time series', function() { + expect(result.length).to.be(2); + expect(result[0].target).to.be('prod.cpu.A'); + expect(result[0].datapoints[0][0]).to.be(10); + expect(result[0].datapoints[0][1]).to.be(1402596000); + expect(result[0].datapoints[1][0]).to.be(11); + expect(result[0].datapoints[1][1]).to.be(1402596001); + + expect(result[1].target).to.be('prod.cpu.B'); + expect(result[1].datapoints[0][0]).to.be(5); + expect(result[1].datapoints[0][1]).to.be(1402596000); + expect(result[1].datapoints[1][0]).to.be(6); + expect(result[1].datapoints[1][1]).to.be(1402596001); + }); + + }); + + }); + + describe("when creating annotations from influxdb response", function() { + describe('given column mapping for all columns', function() { + var series = new InfluxSeries({ + seriesList: [ + { + columns: ['time', 'text', 'sequence_number', 'title', 'tags'], + name: 'events1', + points: [[1402596000000, 'some text', 1, 'Hello', 'B'], [1402596001000, 'asd', 2, 'Hello2', 'B']] + } + ], + annotation: { + query: 'select', + titleColumn: 'title', + tagsColumn: 'tags', + textColumn: 'text', + } + }); + + var result = series.getAnnotations(); + + it(' should generate 2 annnotations ', function() { + expect(result.length).to.be(2); + expect(result[0].annotation.query).to.be('select'); + expect(result[0].title).to.be('Hello'); + expect(result[0].time).to.be(1402596000000); + expect(result[0].tags).to.be('B'); + expect(result[0].text).to.be('some text'); + }); + + }); + + describe('given no column mapping', function() { + var series = new InfluxSeries({ + seriesList: [ + { + columns: ['time', 'text', 'sequence_number'], + name: 'events1', + points: [[1402596000000, 'some text', 1]] + } + ], + annotation: { query: 'select' } + }); + + var result = series.getAnnotations(); + + it('should generate 1 annnotation', function() { + expect(result.length).to.be(1); + expect(result[0].title).to.be('some text'); + expect(result[0].time).to.be(1402596000000); + expect(result[0].tags).to.be(undefined); + expect(result[0].text).to.be(undefined); + }); + + }); + + }); + +}); diff --git a/public/test/test-main.js b/public/test/test-main.js index 86196069265..f064b81bab2 100644 --- a/public/test/test-main.js +++ b/public/test/test-main.js @@ -125,6 +125,7 @@ require([ 'specs/graphiteTargetCtrl-specs', 'specs/graphiteDatasource-specs', 'specs/influxSeries-specs', + 'specs/influxSeries08-specs', 'specs/influxQueryBuilder-specs', 'specs/influx09-querybuilder-specs', 'specs/influxdb-datasource-specs', From e9f38b9fc0d27c5f1ad4945fed19fd9e5b426f19 Mon Sep 17 00:00:00 2001 From: Dieter Plaetinck Date: Mon, 18 May 2015 10:01:58 -0400 Subject: [PATCH 14/89] no unbound recursion in publish() unbound recursion approach can blow up call stack, and - I think - allocate memory unboundedly as well. We can simply loop until err != nil I didn't actually test this live, though tests succeed --- pkg/services/eventpublisher/eventpublisher.go | 27 ++++++++++--------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/pkg/services/eventpublisher/eventpublisher.go b/pkg/services/eventpublisher/eventpublisher.go index 14e527b2cc7..2854b63a9a5 100644 --- a/pkg/services/eventpublisher/eventpublisher.go +++ b/pkg/services/eventpublisher/eventpublisher.go @@ -109,25 +109,26 @@ func Setup() error { } func publish(routingKey string, msgString []byte) { - err := channel.Publish( - exchange, //exchange - routingKey, // routing key - false, // mandatory - false, // immediate - amqp.Publishing{ - ContentType: "application/json", - Body: msgString, - }, - ) - if err != nil { + for { + err := channel.Publish( + exchange, //exchange + routingKey, // routing key + false, // mandatory + false, // immediate + amqp.Publishing{ + ContentType: "application/json", + Body: msgString, + }, + ) + if err == nil { + return + } // failures are most likely because the connection was lost. // the connection will be re-established, so just keep // retrying every 2seconds until we successfully publish. time.Sleep(2 * time.Second) fmt.Println("publish failed, retrying.") - publish(routingKey, msgString) } - return } func eventListener(event interface{}) error { From 5270c4bc740371468b0af536a20d28935eb72fd0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 18 May 2015 17:28:15 +0200 Subject: [PATCH 15/89] refactorin api code for user routes, preparation for admin improvements, #2014 --- pkg/api/api.go | 12 +- pkg/api/common.go | 111 ++++++++++++++++++ pkg/api/index.go | 2 +- pkg/api/user.go | 45 ++++--- pkg/models/org.go | 7 +- .../features/admin/partials/edit_user.html | 4 +- .../app/features/admin/partials/new_user.html | 6 +- .../features/profile/partials/profile.html | 4 +- 8 files changed, 159 insertions(+), 32 deletions(-) create mode 100644 pkg/api/common.go diff --git a/pkg/api/api.go b/pkg/api/api.go index 14e693752b9..cfc1f50b98e 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -55,15 +55,21 @@ func Register(r *macaron.Macaron) { r.Group("/api", func() { // user r.Group("/user", func() { - r.Get("/", GetUser) + r.Get("/", wrap(GetSignedInUser)) r.Put("/", bind(m.UpdateUserCommand{}), UpdateUser) r.Post("/using/:id", UserSetUsingOrg) - r.Get("/orgs", GetUserOrgList) + r.Get("/orgs", wrap(GetSignedInUserOrgList)) r.Post("/stars/dashboard/:id", StarDashboard) r.Delete("/stars/dashboard/:id", UnstarDashboard) r.Put("/password", bind(m.ChangeUserPasswordCommand{}), ChangeUserPassword) }) + // users + r.Group("/users", func() { + r.Get("/:id/", wrap(GetUserById)) + r.Get("/:id/org", wrap(GetUserOrgList)) + }, reqGrafanaAdmin) + // account r.Group("/org", func() { r.Get("/", GetOrg) @@ -127,5 +133,5 @@ func Register(r *macaron.Macaron) { // rendering r.Get("/render/*", reqSignedIn, RenderToPng) - r.NotFound(NotFound) + r.NotFound(NotFoundHandler) } diff --git a/pkg/api/common.go b/pkg/api/common.go new file mode 100644 index 00000000000..8757318159e --- /dev/null +++ b/pkg/api/common.go @@ -0,0 +1,111 @@ +package api + +import ( + "encoding/json" + "net/http" + + "github.com/Unknwon/macaron" + "github.com/grafana/grafana/pkg/log" + "github.com/grafana/grafana/pkg/metrics" + "github.com/grafana/grafana/pkg/middleware" + "github.com/grafana/grafana/pkg/setting" +) + +var ( + NotFound = ApiError(404, "Not found", nil) + ServerError = ApiError(500, "Server error", nil) +) + +type Response interface { + WriteTo(out http.ResponseWriter) +} + +type NormalResponse struct { + status int + body []byte + header http.Header +} + +func wrap(action func(c *middleware.Context) Response) macaron.Handler { + return func(c *middleware.Context) { + res := action(c) + if res == nil { + res = ServerError + } + res.WriteTo(c.Resp) + } +} + +func (r *NormalResponse) WriteTo(out http.ResponseWriter) { + header := out.Header() + for k, v := range r.header { + header[k] = v + } + out.WriteHeader(r.status) + out.Write(r.body) +} + +func (r *NormalResponse) Cache(ttl string) *NormalResponse { + return r.Header("Cache-Control", "public,max-age="+ttl) +} + +func (r *NormalResponse) Header(key, value string) *NormalResponse { + r.header.Set(key, value) + return r +} + +// functions to create responses + +func Empty(status int) *NormalResponse { + return Respond(status, nil) +} + +func Json(status int, body interface{}) *NormalResponse { + return Respond(status, body).Header("Content-Type", "application/json") +} + +func ApiError(status int, message string, err error) *NormalResponse { + resp := make(map[string]interface{}) + + if err != nil { + log.Error(4, "%s: %v", message, err) + if setting.Env != setting.PROD { + resp["error"] = err.Error() + } + } + + switch status { + case 404: + resp["message"] = "Not Found" + metrics.M_Api_Status_500.Inc(1) + case 500: + metrics.M_Api_Status_404.Inc(1) + resp["message"] = "Internal Server Error" + } + + if message != "" { + resp["message"] = message + } + + return Json(status, resp) +} + +func Respond(status int, body interface{}) *NormalResponse { + var b []byte + var err error + switch t := body.(type) { + case []byte: + b = t + case string: + b = []byte(t) + default: + if b, err = json.Marshal(body); err != nil { + return ApiError(500, "body json marshal", err) + } + } + return &NormalResponse{ + body: b, + status: status, + header: make(http.Header), + } +} diff --git a/pkg/api/index.go b/pkg/api/index.go index 386bb3351df..8f486c4b785 100644 --- a/pkg/api/index.go +++ b/pkg/api/index.go @@ -59,7 +59,7 @@ func Index(c *middleware.Context) { c.HTML(200, "index") } -func NotFound(c *middleware.Context) { +func NotFoundHandler(c *middleware.Context) { if c.IsApiRequest() { c.JsonApiErr(404, "Not found", nil) return diff --git a/pkg/api/user.go b/pkg/api/user.go index 9d870a10a1b..e7cc8ff0366 100644 --- a/pkg/api/user.go +++ b/pkg/api/user.go @@ -7,15 +7,24 @@ import ( "github.com/grafana/grafana/pkg/util" ) -func GetUser(c *middleware.Context) { - query := m.GetUserProfileQuery{UserId: c.UserId} +// GET /api/user (current authenticated user) +func GetSignedInUser(c *middleware.Context) Response { + return getUserUserProfile(c.UserId) +} + +// GET /api/user/:id +func GetUserById(c *middleware.Context) Response { + return getUserUserProfile(c.ParamsInt64(":id")) +} + +func getUserUserProfile(userId int64) Response { + query := m.GetUserProfileQuery{UserId: userId} if err := bus.Dispatch(&query); err != nil { - c.JsonApiErr(500, "Failed to get user", err) - return + return ApiError(500, "Failed to get user", err) } - c.JSON(200, query.Result) + return Json(200, query.Result) } func UpdateUser(c *middleware.Context, cmd m.UpdateUserCommand) { @@ -29,22 +38,24 @@ func UpdateUser(c *middleware.Context, cmd m.UpdateUserCommand) { c.JsonOK("User updated") } -func GetUserOrgList(c *middleware.Context) { - query := m.GetUserOrgListQuery{UserId: c.UserId} +// GET /api/user/orgs +func GetSignedInUserOrgList(c *middleware.Context) Response { + return getUserOrgList(c.UserId) +} + +// GET /api/user/:id/orgs +func GetUserOrgList(c *middleware.Context) Response { + return getUserOrgList(c.ParamsInt64(":id")) +} + +func getUserOrgList(userId int64) Response { + query := m.GetUserOrgListQuery{UserId: userId} if err := bus.Dispatch(&query); err != nil { - c.JsonApiErr(500, "Failed to get user organizations", err) - return + return ApiError(500, "Faile to get user organziations", err) } - for _, ac := range query.Result { - if ac.OrgId == c.OrgId { - ac.IsUsing = true - break - } - } - - c.JSON(200, query.Result) + return Json(200, query.Result) } func validateUsingOrg(userId int64, orgId int64) bool { diff --git a/pkg/models/org.go b/pkg/models/org.go index ab6d97b9ae8..a8ee08ea69b 100644 --- a/pkg/models/org.go +++ b/pkg/models/org.go @@ -58,8 +58,7 @@ type OrgDTO struct { } type UserOrgDTO struct { - OrgId int64 `json:"orgId"` - Name string `json:"name"` - Role RoleType `json:"role"` - IsUsing bool `json:"isUsing"` + OrgId int64 `json:"orgId"` + Name string `json:"name"` + Role RoleType `json:"role"` } diff --git a/public/app/features/admin/partials/edit_user.html b/public/app/features/admin/partials/edit_user.html index 9b2a18fd010..c82d7705b8d 100644 --- a/public/app/features/admin/partials/edit_user.html +++ b/public/app/features/admin/partials/edit_user.html @@ -25,7 +25,7 @@
    -
    +
    • Email @@ -36,7 +36,7 @@
    -
    +
    • Username diff --git a/public/app/features/admin/partials/new_user.html b/public/app/features/admin/partials/new_user.html index 48f78fb76b6..73877f9bda4 100644 --- a/public/app/features/admin/partials/new_user.html +++ b/public/app/features/admin/partials/new_user.html @@ -24,7 +24,7 @@
    -
    +
    • Email @@ -35,7 +35,7 @@
    -
    +
    • Username @@ -46,7 +46,7 @@
    -
    +
    • Password diff --git a/public/app/features/profile/partials/profile.html b/public/app/features/profile/partials/profile.html index d8758ae9c09..5512245b275 100644 --- a/public/app/features/profile/partials/profile.html +++ b/public/app/features/profile/partials/profile.html @@ -71,10 +71,10 @@ Name: {{org.name}} Role: {{org.role}} - + Current - + Select From 62e8841e8cf2f63ec3a18eb6a6dd9c16d3b57a5f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 18 May 2015 17:54:12 +0200 Subject: [PATCH 16/89] Fixed spelling of Peta unit Quadr --- public/app/components/kbn.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/app/components/kbn.js b/public/app/components/kbn.js index ef1e45c23d3..15a4c7637c3 100644 --- a/public/app/components/kbn.js +++ b/public/app/components/kbn.js @@ -384,7 +384,7 @@ function($, _, moment) { kbn.valueFormats.gbytes = kbn.formatFuncCreator(1024, [' GiB', ' TiB', ' PiB', ' EiB', ' ZiB', ' YiB']); kbn.valueFormats.bps = kbn.formatFuncCreator(1000, [' bps', ' Kbps', ' Mbps', ' Gbps', ' Tbps', ' Pbps', ' Ebps', ' Zbps', ' Ybps']); kbn.valueFormats.Bps = kbn.formatFuncCreator(1000, [' Bps', ' KBps', ' MBps', ' GBps', ' TBps', ' PBps', ' EBps', ' ZBps', ' YBps']); - kbn.valueFormats.short = kbn.formatFuncCreator(1000, ['', ' K', ' Mil', ' Bil', ' Tri', ' Qaudr', ' Quint', ' Sext', ' Sept']); + kbn.valueFormats.short = kbn.formatFuncCreator(1000, ['', ' K', ' Mil', ' Bil', ' Tri', ' Quadr', ' Quint', ' Sext', ' Sept']); kbn.valueFormats.joule = kbn.formatFuncCreator(1000, [' J', ' kJ', ' MJ', ' GJ', ' TJ', ' PJ', ' EJ', ' ZJ', ' YJ']); kbn.valueFormats.amp = kbn.formatFuncCreator(1000, [' A', ' kA', ' MA', ' GA', ' TA', ' PA', ' EA', ' ZA', ' YA']); kbn.valueFormats.volt = kbn.formatFuncCreator(1000, [' V', ' kV', ' MV', ' GV', ' TV', ' PV', ' EV', ' ZV', ' YV']); From fbc6bb21123d3bb97efe61c19abe7896e3a741a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 18 May 2015 19:06:19 +0200 Subject: [PATCH 17/89] More refactoring of user http api, trying to reuse handlers for sign in user and admin operations --- pkg/api/admin_users.go | 46 ------------------- pkg/api/api.go | 7 ++- pkg/api/common.go | 17 +++++-- pkg/api/user.go | 26 +++++++++-- pkg/services/sqlstore/user.go | 9 ++-- .../app/features/admin/adminEditUserCtrl.js | 4 +- 6 files changed, 45 insertions(+), 64 deletions(-) diff --git a/pkg/api/admin_users.go b/pkg/api/admin_users.go index f7e8fca2b5e..23cfc826ed2 100644 --- a/pkg/api/admin_users.go +++ b/pkg/api/admin_users.go @@ -19,26 +19,6 @@ func AdminSearchUsers(c *middleware.Context) { c.JSON(200, query.Result) } -func AdminGetUser(c *middleware.Context) { - userId := c.ParamsInt64(":id") - - query := m.GetUserByIdQuery{Id: userId} - - if err := bus.Dispatch(&query); err != nil { - c.JsonApiErr(500, "Failed to fetch user", err) - return - } - - result := dtos.AdminUserListItem{ - Name: query.Result.Name, - Email: query.Result.Email, - Login: query.Result.Login, - IsGrafanaAdmin: query.Result.IsAdmin, - } - - c.JSON(200, result) -} - func AdminCreateUser(c *middleware.Context, form dtos.AdminCreateUserForm) { cmd := m.CreateUserCommand{ Login: form.Login, @@ -70,32 +50,6 @@ func AdminCreateUser(c *middleware.Context, form dtos.AdminCreateUserForm) { c.JsonOK("User created") } -func AdminUpdateUser(c *middleware.Context, form dtos.AdminUpdateUserForm) { - userId := c.ParamsInt64(":id") - - cmd := m.UpdateUserCommand{ - UserId: userId, - Login: form.Login, - Email: form.Email, - Name: form.Name, - } - - if len(cmd.Login) == 0 { - cmd.Login = cmd.Email - if len(cmd.Login) == 0 { - c.JsonApiErr(400, "Validation error, need specify either username or email", nil) - return - } - } - - if err := bus.Dispatch(&cmd); err != nil { - c.JsonApiErr(500, "failed to update user", err) - return - } - - c.JsonOK("User updated") -} - func AdminUpdateUserPassword(c *middleware.Context, form dtos.AdminUpdateUserPasswordForm) { userId := c.ParamsInt64(":id") diff --git a/pkg/api/api.go b/pkg/api/api.go index cfc1f50b98e..a84c753c7ba 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -56,7 +56,7 @@ func Register(r *macaron.Macaron) { // user r.Group("/user", func() { r.Get("/", wrap(GetSignedInUser)) - r.Put("/", bind(m.UpdateUserCommand{}), UpdateUser) + r.Put("/", bind(m.UpdateUserCommand{}), wrap(UpdateSignedInUser)) r.Post("/using/:id", UserSetUsingOrg) r.Get("/orgs", wrap(GetSignedInUserOrgList)) r.Post("/stars/dashboard/:id", StarDashboard) @@ -66,8 +66,9 @@ func Register(r *macaron.Macaron) { // users r.Group("/users", func() { - r.Get("/:id/", wrap(GetUserById)) + r.Get("/:id", wrap(GetUserById)) r.Get("/:id/org", wrap(GetUserOrgList)) + r.Put("/:id", bind(m.UpdateUserCommand{}), wrap(UpdateUser)) }, reqGrafanaAdmin) // account @@ -122,9 +123,7 @@ func Register(r *macaron.Macaron) { r.Group("/api/admin", func() { r.Get("/settings", AdminGetSettings) r.Get("/users", AdminSearchUsers) - r.Get("/users/:id", AdminGetUser) r.Post("/users", bind(dtos.AdminCreateUserForm{}), AdminCreateUser) - r.Put("/users/:id/details", bind(dtos.AdminUpdateUserForm{}), AdminUpdateUser) r.Put("/users/:id/password", bind(dtos.AdminUpdateUserPasswordForm{}), AdminUpdateUserPassword) r.Put("/users/:id/permissions", bind(dtos.AdminUpdateUserPermissionsForm{}), AdminUpdateUserPermissions) r.Delete("/users/:id", AdminDeleteUser) diff --git a/pkg/api/common.go b/pkg/api/common.go index 8757318159e..4d8b3c28032 100644 --- a/pkg/api/common.go +++ b/pkg/api/common.go @@ -26,12 +26,17 @@ type NormalResponse struct { header http.Header } -func wrap(action func(c *middleware.Context) Response) macaron.Handler { +func wrap(action interface{}) macaron.Handler { + return func(c *middleware.Context) { - res := action(c) - if res == nil { + var res Response + val, err := c.Invoke(action) + if err == nil && val != nil && len(val) > 0 { + res = val[0].Interface().(Response) + } else { res = ServerError } + res.WriteTo(c.Resp) } } @@ -64,6 +69,12 @@ func Json(status int, body interface{}) *NormalResponse { return Respond(status, body).Header("Content-Type", "application/json") } +func ApiSuccess(message string) *NormalResponse { + resp := make(map[string]interface{}) + resp["message"] = message + return Respond(200, resp) +} + func ApiError(status int, message string, err error) *NormalResponse { resp := make(map[string]interface{}) diff --git a/pkg/api/user.go b/pkg/api/user.go index e7cc8ff0366..1b5654ce1f0 100644 --- a/pkg/api/user.go +++ b/pkg/api/user.go @@ -27,15 +27,31 @@ func getUserUserProfile(userId int64) Response { return Json(200, query.Result) } -func UpdateUser(c *middleware.Context, cmd m.UpdateUserCommand) { +// POST /api/user +func UpdateSignedInUser(c *middleware.Context, cmd m.UpdateUserCommand) Response { cmd.UserId = c.UserId + return handleUpdateUser(cmd) +} - if err := bus.Dispatch(&cmd); err != nil { - c.JsonApiErr(400, "Failed to update user", err) - return +// POST /api/users/:id +func UpdateUser(c *middleware.Context, cmd m.UpdateUserCommand) Response { + cmd.UserId = c.ParamsInt64(":id") + return handleUpdateUser(cmd) +} + +func handleUpdateUser(cmd m.UpdateUserCommand) Response { + if len(cmd.Login) == 0 { + cmd.Login = cmd.Email + if len(cmd.Login) == 0 { + return ApiError(400, "Validation error, need specify either username or email", nil) + } } - c.JsonOK("User updated") + if err := bus.Dispatch(&cmd); err != nil { + return ApiError(500, "failed to update user", err) + } + + return ApiSuccess("User updated") } // GET /api/user/orgs diff --git a/pkg/services/sqlstore/user.go b/pkg/services/sqlstore/user.go index 663496427ba..b74422b69ca 100644 --- a/pkg/services/sqlstore/user.go +++ b/pkg/services/sqlstore/user.go @@ -231,10 +231,11 @@ func GetUserProfile(query *m.GetUserProfileQuery) error { } query.Result = m.UserProfileDTO{ - Name: user.Name, - Email: user.Email, - Login: user.Login, - Theme: user.Theme, + Name: user.Name, + Email: user.Email, + Login: user.Login, + Theme: user.Theme, + IsGrafanaAdmin: user.IsAdmin, } return err diff --git a/public/app/features/admin/adminEditUserCtrl.js b/public/app/features/admin/adminEditUserCtrl.js index 19deac532ea..72a7dba0229 100644 --- a/public/app/features/admin/adminEditUserCtrl.js +++ b/public/app/features/admin/adminEditUserCtrl.js @@ -17,7 +17,7 @@ function (angular) { }; $scope.getUser = function(id) { - backendSrv.get('/api/admin/users/' + id).then(function(user) { + backendSrv.get('/api/users/' + id).then(function(user) { $scope.user = user; $scope.user_id = id; $scope.permissions.isGrafanaAdmin = user.isGrafanaAdmin; @@ -52,7 +52,7 @@ function (angular) { $scope.update = function() { if (!$scope.userForm.$valid) { return; } - backendSrv.put('/api/admin/users/' + $scope.user_id + '/details', $scope.user).then(function() { + backendSrv.put('/api/users/' + $scope.user_id, $scope.user).then(function() { $location.path('/admin/users'); }); }; From f81bde5643722ebb3e8290e8208347d288f4d935 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 18 May 2015 21:23:40 +0200 Subject: [PATCH 18/89] Refactoring some api handlers to use the new Response return object --- pkg/api/api.go | 6 ++--- pkg/api/apikey.go | 28 ++++++++++------------- public/app/features/org/orgApiKeysCtrl.js | 2 ++ 3 files changed, 17 insertions(+), 19 deletions(-) diff --git a/pkg/api/api.go b/pkg/api/api.go index a84c753c7ba..0503ac61a3c 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -84,9 +84,9 @@ func Register(r *macaron.Macaron) { // auth api keys r.Group("/auth/keys", func() { - r.Get("/", GetApiKeys) - r.Post("/", bind(m.AddApiKeyCommand{}), AddApiKey) - r.Delete("/:id", DeleteApiKey) + r.Get("/", wrap(GetApiKeys)) + r.Post("/", bind(m.AddApiKeyCommand{}), wrap(AddApiKey)) + r.Delete("/:id", wrap(DeleteApiKey)) }, reqAccountAdmin) // Data sources diff --git a/pkg/api/apikey.go b/pkg/api/apikey.go index 237fdc48ab0..b2097104aba 100644 --- a/pkg/api/apikey.go +++ b/pkg/api/apikey.go @@ -8,12 +8,11 @@ import ( m "github.com/grafana/grafana/pkg/models" ) -func GetApiKeys(c *middleware.Context) { +func GetApiKeys(c *middleware.Context) Response { query := m.GetApiKeysQuery{OrgId: c.OrgId} if err := bus.Dispatch(&query); err != nil { - c.JsonApiErr(500, "Failed to list api keys", err) - return + return ApiError(500, "Failed to list api keys", err) } result := make([]*m.ApiKeyDTO, len(query.Result)) @@ -24,27 +23,26 @@ func GetApiKeys(c *middleware.Context) { Role: t.Role, } } - c.JSON(200, result) + + return Json(200, result) } -func DeleteApiKey(c *middleware.Context) { +func DeleteApiKey(c *middleware.Context) Response { id := c.ParamsInt64(":id") cmd := &m.DeleteApiKeyCommand{Id: id, OrgId: c.OrgId} err := bus.Dispatch(cmd) if err != nil { - c.JsonApiErr(500, "Failed to delete API key", err) - return + return ApiError(500, "Failed to delete API key", err) } - c.JsonOK("API key deleted") + return ApiSuccess("API key deleted") } -func AddApiKey(c *middleware.Context, cmd m.AddApiKeyCommand) { +func AddApiKey(c *middleware.Context, cmd m.AddApiKeyCommand) Response { if !cmd.Role.IsValid() { - c.JsonApiErr(400, "Invalid role specified", nil) - return + return ApiError(400, "Invalid role specified", nil) } cmd.OrgId = c.OrgId @@ -53,14 +51,12 @@ func AddApiKey(c *middleware.Context, cmd m.AddApiKeyCommand) { cmd.Key = newKeyInfo.HashedKey if err := bus.Dispatch(&cmd); err != nil { - c.JsonApiErr(500, "Failed to add API key", err) - return + return ApiError(500, "Failed to add API key", err) } result := &dtos.NewApiKeyResult{ Name: cmd.Result.Name, - Key: newKeyInfo.ClientSecret, - } + Key: newKeyInfo.ClientSecret} - c.JSON(200, result) + return Json(200, result) } diff --git a/public/app/features/org/orgApiKeysCtrl.js b/public/app/features/org/orgApiKeysCtrl.js index a8b05155401..918f57b42e8 100644 --- a/public/app/features/org/orgApiKeysCtrl.js +++ b/public/app/features/org/orgApiKeysCtrl.js @@ -35,6 +35,8 @@ function (angular) { src: './app/features/org/partials/apikeyModal.html', scope: modalScope }); + + $scope.getTokens(); }); }; From bfe6d5434e70f5ea1c1af6acbb568d5878ab7843 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Tue, 19 May 2015 08:46:45 +0200 Subject: [PATCH 19/89] Fixed placeholder text in templating editor --- public/app/features/templating/partials/editor.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/public/app/features/templating/partials/editor.html b/public/app/features/templating/partials/editor.html index 11769e34dfc..a0549414b6a 100644 --- a/public/app/features/templating/partials/editor.html +++ b/public/app/features/templating/partials/editor.html @@ -65,7 +65,7 @@ Name
    • - +
    • Type @@ -139,7 +139,7 @@ Query
    • - +
    From bf9e51928df97ff0bc2895c869697648dd660def Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Tue, 19 May 2015 09:02:37 +0200 Subject: [PATCH 20/89] Fix to signed in user when user <-> org link is gone --- pkg/services/sqlstore/user.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/pkg/services/sqlstore/user.go b/pkg/services/sqlstore/user.go index b74422b69ca..9f79081783b 100644 --- a/pkg/services/sqlstore/user.go +++ b/pkg/services/sqlstore/user.go @@ -283,6 +283,11 @@ func GetSignedInUser(query *m.GetSignedInUserQuery) error { return m.ErrUserNotFound } + if user.OrgRole == "" { + user.OrgId = -1 + user.OrgName = "Org missing" + } + query.Result = &user return err } From 74bf1f23fb8e217e82aff0c9d7b5e697e0e6a9e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Tue, 19 May 2015 09:09:21 +0200 Subject: [PATCH 21/89] Small progress on #2014 --- pkg/api/api.go | 2 +- public/app/features/admin/adminEditUserCtrl.js | 7 +++++++ .../app/features/admin/partials/edit_user.html | 16 ++++++++++++++++ 3 files changed, 24 insertions(+), 1 deletion(-) diff --git a/pkg/api/api.go b/pkg/api/api.go index 0503ac61a3c..f45da26d9c2 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -67,7 +67,7 @@ func Register(r *macaron.Macaron) { // users r.Group("/users", func() { r.Get("/:id", wrap(GetUserById)) - r.Get("/:id/org", wrap(GetUserOrgList)) + r.Get("/:id/orgs", wrap(GetUserOrgList)) r.Put("/:id", bind(m.UpdateUserCommand{}), wrap(UpdateUser)) }, reqGrafanaAdmin) diff --git a/public/app/features/admin/adminEditUserCtrl.js b/public/app/features/admin/adminEditUserCtrl.js index 72a7dba0229..dc8839f0f84 100644 --- a/public/app/features/admin/adminEditUserCtrl.js +++ b/public/app/features/admin/adminEditUserCtrl.js @@ -13,6 +13,7 @@ function (angular) { $scope.init = function() { if ($routeParams.id) { $scope.getUser($routeParams.id); + $scope.getUserOrgs($routeParams.id); } }; @@ -49,6 +50,12 @@ function (angular) { }); }; + $scope.getUserOrgs = function(id) { + backendSrv.get('/api/users/' + id + '/orgs').then(function(orgs) { + $scope.orgs = orgs; + }); + }; + $scope.update = function() { if (!$scope.userForm.$valid) { return; } diff --git a/public/app/features/admin/partials/edit_user.html b/public/app/features/admin/partials/edit_user.html index c82d7705b8d..712aee26d5d 100644 --- a/public/app/features/admin/partials/edit_user.html +++ b/public/app/features/admin/partials/edit_user.html @@ -94,5 +94,21 @@
    +

    + Organizations +

    + + + + + + + +
    Name: {{org.name}}Role: {{org.role}} + + Current + +
    +
    From 788e7fd36d09dfa5487e536d715d733be33bb82e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Tue, 19 May 2015 10:16:32 +0200 Subject: [PATCH 22/89] Lots of api refactoring for org routes, #2014 --- pkg/api/api.go | 35 ++++-- pkg/api/org.go | 50 ++++++--- pkg/api/org_users.go | 105 ++++++++++++------ pkg/models/user.go | 1 + pkg/services/sqlstore/user.go | 1 + .../features/admin/partials/edit_user.html | 25 +++-- public/app/features/admin/partials/users.html | 2 +- public/app/features/org/newOrgCtrl.js | 2 +- .../app/features/org/partials/orgUsers.html | 2 +- .../datasource/influxdb/queryBuilder.js | 2 - .../test/specs/influx09-querybuilder-specs.js | 15 ++- 11 files changed, 155 insertions(+), 85 deletions(-) diff --git a/pkg/api/api.go b/pkg/api/api.go index f45da26d9c2..1c63ad3a541 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -13,7 +13,7 @@ func Register(r *macaron.Macaron) { reqSignedIn := middleware.Auth(&middleware.AuthOptions{ReqSignedIn: true}) reqGrafanaAdmin := middleware.Auth(&middleware.AuthOptions{ReqSignedIn: true, ReqGrafanaAdmin: true}) reqEditorRole := middleware.RoleAuth(m.ROLE_EDITOR, m.ROLE_ADMIN) - reqAccountAdmin := middleware.RoleAuth(m.ROLE_ADMIN) + regOrgAdmin := middleware.RoleAuth(m.ROLE_ADMIN) bind := binding.Bind // not logged in views @@ -71,23 +71,34 @@ func Register(r *macaron.Macaron) { r.Put("/:id", bind(m.UpdateUserCommand{}), wrap(UpdateUser)) }, reqGrafanaAdmin) - // account + // current org r.Group("/org", func() { - r.Get("/", GetOrg) - r.Post("/", bind(m.CreateOrgCommand{}), CreateOrg) - r.Put("/", bind(m.UpdateOrgCommand{}), UpdateOrg) - r.Post("/users", bind(m.AddOrgUserCommand{}), AddOrgUser) - r.Get("/users", GetOrgUsers) - r.Patch("/users/:id", bind(m.UpdateOrgUserCommand{}), UpdateOrgUser) - r.Delete("/users/:id", RemoveOrgUser) - }, reqAccountAdmin) + r.Get("/", wrap(GetOrgCurrent)) + r.Put("/", bind(m.UpdateOrgCommand{}), wrap(UpdateOrgCurrent)) + r.Post("/users", bind(m.AddOrgUserCommand{}), wrap(AddOrgUserToCurrentOrg)) + r.Get("/users", wrap(GetOrgUsersForCurrentOrg)) + r.Patch("/users/:userId", bind(m.UpdateOrgUserCommand{}), wrap(UpdateOrgUserForCurrentOrg)) + r.Delete("/users/:userId", wrap(RemoveOrgUserForCurrentOrg)) + }, regOrgAdmin) + + // create new org + r.Post("/orgs", bind(m.CreateOrgCommand{}), wrap(CreateOrg)) + + // orgs (admin routes) + r.Group("/orgs/:orgId", func() { + r.Put("/", bind(m.UpdateOrgCommand{}), wrap(UpdateOrg)) + r.Get("/users", wrap(GetOrgUsers)) + r.Post("/users", bind(m.AddOrgUserCommand{}), wrap(AddOrgUser)) + r.Patch("/users/:userId", bind(m.UpdateOrgUserCommand{}), wrap(UpdateOrgUser)) + r.Delete("/users/:userId", wrap(RemoveOrgUser)) + }, reqGrafanaAdmin) // auth api keys r.Group("/auth/keys", func() { r.Get("/", wrap(GetApiKeys)) r.Post("/", bind(m.AddApiKeyCommand{}), wrap(AddApiKey)) r.Delete("/:id", wrap(DeleteApiKey)) - }, reqAccountAdmin) + }, regOrgAdmin) // Data sources r.Group("/datasources", func() { @@ -98,7 +109,7 @@ func Register(r *macaron.Macaron) { r.Delete("/:id", DeleteDataSource) r.Get("/:id", GetDataSourceById) r.Get("/plugins", GetDataSourcePlugins) - }, reqAccountAdmin) + }, regOrgAdmin) r.Get("/frontend/settings/", GetFrontendSettings) r.Any("/datasources/proxy/:id/*", reqSignedIn, ProxyDataSourceRequest) diff --git a/pkg/api/org.go b/pkg/api/org.go index ac8727c9e49..5b9e1a9aef8 100644 --- a/pkg/api/org.go +++ b/pkg/api/org.go @@ -8,17 +8,25 @@ import ( "github.com/grafana/grafana/pkg/setting" ) -func GetOrg(c *middleware.Context) { - query := m.GetOrgByIdQuery{Id: c.OrgId} +// GET /api/org +func GetOrgCurrent(c *middleware.Context) Response { + return getOrgHelper(c.OrgId) +} + +// GET /api/orgs/:orgId +func GetOrgById(c *middleware.Context) Response { + return getOrgHelper(c.ParamsInt64(":orgId")) +} + +func getOrgHelper(orgId int64) Response { + query := m.GetOrgByIdQuery{Id: orgId} if err := bus.Dispatch(&query); err != nil { if err == m.ErrOrgNotFound { - c.JsonApiErr(404, "Organization not found", err) - return + return ApiError(404, "Organization not found", err) } - c.JsonApiErr(500, "Failed to get organization", err) - return + return ApiError(500, "Failed to get organization", err) } org := m.OrgDTO{ @@ -26,33 +34,41 @@ func GetOrg(c *middleware.Context) { Name: query.Result.Name, } - c.JSON(200, &org) + return Json(200, &org) } -func CreateOrg(c *middleware.Context, cmd m.CreateOrgCommand) { +// POST /api/orgs +func CreateOrg(c *middleware.Context, cmd m.CreateOrgCommand) Response { if !setting.AllowUserOrgCreate && !c.IsGrafanaAdmin { - c.JsonApiErr(401, "Access denied", nil) - return + return ApiError(401, "Access denied", nil) } cmd.UserId = c.UserId if err := bus.Dispatch(&cmd); err != nil { - c.JsonApiErr(500, "Failed to create organization", err) - return + return ApiError(500, "Failed to create organization", err) } metrics.M_Api_Org_Create.Inc(1) - c.JsonOK("Organization created") + return ApiSuccess("Organization created") } -func UpdateOrg(c *middleware.Context, cmd m.UpdateOrgCommand) { +// PUT /api/org +func UpdateOrgCurrent(c *middleware.Context, cmd m.UpdateOrgCommand) Response { cmd.OrgId = c.OrgId + return updateOrgHelper(cmd) +} +// PUT /api/orgs/:orgId +func UpdateOrg(c *middleware.Context, cmd m.UpdateOrgCommand) Response { + cmd.OrgId = c.ParamsInt64(":orgId") + return updateOrgHelper(cmd) +} + +func updateOrgHelper(cmd m.UpdateOrgCommand) Response { if err := bus.Dispatch(&cmd); err != nil { - c.JsonApiErr(500, "Failed to update organization", err) - return + return ApiError(500, "Failed to update organization", err) } - c.JsonOK("Organization updated") + return ApiSuccess("Organization updated") } diff --git a/pkg/api/org_users.go b/pkg/api/org_users.go index 8a372c791b9..c88df600450 100644 --- a/pkg/api/org_users.go +++ b/pkg/api/org_users.go @@ -6,77 +6,112 @@ import ( m "github.com/grafana/grafana/pkg/models" ) -func AddOrgUser(c *middleware.Context, cmd m.AddOrgUserCommand) { +// POST /api/org/users +func AddOrgUserToCurrentOrg(c *middleware.Context, cmd m.AddOrgUserCommand) Response { + cmd.OrgId = c.OrgId + return addOrgUserHelper(cmd) +} + +// POST /api/orgs/:orgId/users +func AddOrgUser(c *middleware.Context, cmd m.AddOrgUserCommand) Response { + cmd.OrgId = c.ParamsInt64(":orgId") + return addOrgUserHelper(cmd) +} + +func addOrgUserHelper(cmd m.AddOrgUserCommand) Response { if !cmd.Role.IsValid() { - c.JsonApiErr(400, "Invalid role specified", nil) - return + return ApiError(400, "Invalid role specified", nil) } userQuery := m.GetUserByLoginQuery{LoginOrEmail: cmd.LoginOrEmail} err := bus.Dispatch(&userQuery) if err != nil { - c.JsonApiErr(404, "User not found", nil) - return + return ApiError(404, "User not found", nil) } userToAdd := userQuery.Result - if userToAdd.Id == c.UserId { - c.JsonApiErr(400, "Cannot add yourself as user", nil) - return - } + // if userToAdd.Id == c.UserId { + // return ApiError(400, "Cannot add yourself as user", nil) + // } - cmd.OrgId = c.OrgId cmd.UserId = userToAdd.Id if err := bus.Dispatch(&cmd); err != nil { - c.JsonApiErr(500, "Could not add user to organization", err) - return + return ApiError(500, "Could not add user to organization", err) } - c.JsonOK("User added to organization") + return ApiSuccess("User added to organization") } -func GetOrgUsers(c *middleware.Context) { - query := m.GetOrgUsersQuery{OrgId: c.OrgId} +// GET /api/org/users +func GetOrgUsersForCurrentOrg(c *middleware.Context) Response { + return getOrgUsersHelper(c.OrgId) +} + +// GET /api/orgs/:orgId/users +func GetOrgUsers(c *middleware.Context) Response { + return getOrgUsersHelper(c.ParamsInt64(":orgId")) +} + +func getOrgUsersHelper(orgId int64) Response { + query := m.GetOrgUsersQuery{OrgId: orgId} if err := bus.Dispatch(&query); err != nil { - c.JsonApiErr(500, "Failed to get account user", err) - return + return ApiError(500, "Failed to get account user", err) } - c.JSON(200, query.Result) + return Json(200, query.Result) } -func UpdateOrgUser(c *middleware.Context, cmd m.UpdateOrgUserCommand) { - if !cmd.Role.IsValid() { - c.JsonApiErr(400, "Invalid role specified", nil) - return - } - - cmd.UserId = c.ParamsInt64(":id") +// PATCH /api/org/users/:userId +func UpdateOrgUserForCurrentOrg(c *middleware.Context, cmd m.UpdateOrgUserCommand) Response { cmd.OrgId = c.OrgId + cmd.UserId = c.ParamsInt64(":userId") + return updateOrgUserHelper(cmd) +} + +// PATCH /api/orgs/:orgId/users/:userId +func UpdateOrgUser(c *middleware.Context, cmd m.UpdateOrgUserCommand) Response { + cmd.OrgId = c.ParamsInt64(":orgId") + cmd.UserId = c.ParamsInt64(":userId") + return updateOrgUserHelper(cmd) +} + +func updateOrgUserHelper(cmd m.UpdateOrgUserCommand) Response { + if !cmd.Role.IsValid() { + return ApiError(400, "Invalid role specified", nil) + } if err := bus.Dispatch(&cmd); err != nil { - c.JsonApiErr(500, "Failed update org user", err) - return + return ApiError(500, "Failed update org user", err) } - c.JsonOK("Organization user updated") + return ApiSuccess("Organization user updated") } -func RemoveOrgUser(c *middleware.Context) { - userId := c.ParamsInt64(":id") +// DELETE /api/org/users/:userId +func RemoveOrgUserForCurrentOrg(c *middleware.Context) Response { + userId := c.ParamsInt64(":userId") + return removeOrgUserHelper(c.OrgId, userId) +} - cmd := m.RemoveOrgUserCommand{OrgId: c.OrgId, UserId: userId} +// DELETE /api/orgs/:orgId/users/:userId +func RemoveOrgUser(c *middleware.Context) Response { + userId := c.ParamsInt64(":userId") + orgId := c.ParamsInt64(":orgId") + return removeOrgUserHelper(orgId, userId) +} + +func removeOrgUserHelper(orgId int64, userId int64) Response { + cmd := m.RemoveOrgUserCommand{OrgId: orgId, UserId: userId} if err := bus.Dispatch(&cmd); err != nil { if err == m.ErrLastOrgAdmin { - c.JsonApiErr(400, "Cannot remove last organization admin", nil) - return + return ApiError(400, "Cannot remove last organization admin", nil) } - c.JsonApiErr(500, "Failed to remove user from organization", err) + return ApiError(500, "Failed to remove user from organization", err) } - c.JsonOK("User removed from organization") + return ApiSuccess("User removed from organization") } diff --git a/pkg/models/user.go b/pkg/models/user.go index 68e0001b99c..5efecc8deef 100644 --- a/pkg/models/user.go +++ b/pkg/models/user.go @@ -133,6 +133,7 @@ type UserProfileDTO struct { Name string `json:"name"` Login string `json:"login"` Theme string `json:"theme"` + OrgId int64 `json:"orgId"` IsGrafanaAdmin bool `json:"isGrafanaAdmin"` } diff --git a/pkg/services/sqlstore/user.go b/pkg/services/sqlstore/user.go index 9f79081783b..f5df6f9ff1f 100644 --- a/pkg/services/sqlstore/user.go +++ b/pkg/services/sqlstore/user.go @@ -236,6 +236,7 @@ func GetUserProfile(query *m.GetUserProfileQuery) error { Login: user.Login, Theme: user.Theme, IsGrafanaAdmin: user.IsAdmin, + OrgId: user.OrgId, } return err diff --git a/public/app/features/admin/partials/edit_user.html b/public/app/features/admin/partials/edit_user.html index 712aee26d5d..e72bd5b57c9 100644 --- a/public/app/features/admin/partials/edit_user.html +++ b/public/app/features/admin/partials/edit_user.html @@ -98,17 +98,26 @@ Organizations - +
    + + + + + - - - + +
    NameRole
    Name: {{org.name}}Role: {{org.role}} - - Current - + + {{org.name}} Current + + + + + +
    -
    diff --git a/public/app/features/admin/partials/users.html b/public/app/features/admin/partials/users.html index 14bcd922b5e..6d4f7a8671c 100644 --- a/public/app/features/admin/partials/users.html +++ b/public/app/features/admin/partials/users.html @@ -1,4 +1,4 @@ - +
    diff --git a/public/app/features/templating/partials/editor.html b/public/app/features/templating/partials/editor.html index a0549414b6a..6883410cd01 100644 --- a/public/app/features/templating/partials/editor.html +++ b/public/app/features/templating/partials/editor.html @@ -226,6 +226,42 @@
    +
    +
    +
    Value Groups/Tags
    +
    +
      +
    • + Tags query +
    • +
    • + +
    • +
    +
    +
    +
    +
      +
    • + Tags values query +
    • +
    • + +
    • +
    +
    +
    +
    +
      +
    • + +
    • +
    +
    +
    +
    +
    +
    Preview of values (shows max 20)
    diff --git a/public/app/features/templating/templateValuesSrv.js b/public/app/features/templating/templateValuesSrv.js index 6db5ce595a2..7330be1b5cd 100644 --- a/public/app/features/templating/templateValuesSrv.js +++ b/public/app/features/templating/templateValuesSrv.js @@ -120,7 +120,7 @@ function (angular, _, kbn) { } return datasourceSrv.get(variable.datasource).then(function(datasource) { - return datasource.metricFindQuery(variable.query).then(function (results) { + var queryPromise = datasource.metricFindQuery(variable.query).then(function (results) { variable.options = self.metricNamesToVariableValues(variable, results); if (variable.includeAll) { @@ -138,6 +138,19 @@ function (angular, _, kbn) { return self.setVariableValue(variable, variable.options[0]); }); + + if (variable.useTags) { + return queryPromise.then(function() { + datasource.metricFindQuery(variable.tagsQuery).then(function (results) { + variable.tags = []; + for (var i = 0; i < results.length; i++) { + variable.tags.push(results[i].text); + } + }); + }); + } else { + return queryPromise; + } }); }; From 592330b5a74983f9e094ba9f9a18bdaff549e00e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Wed, 20 May 2015 18:29:20 +0200 Subject: [PATCH 28/89] Expose data source extended properties (jsonData), to the frontend, Closes #2023 --- pkg/api/frontendsettings.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pkg/api/frontendsettings.go b/pkg/api/frontendsettings.go index 3af191a7af7..ea154320608 100644 --- a/pkg/api/frontendsettings.go +++ b/pkg/api/frontendsettings.go @@ -54,6 +54,10 @@ func getFrontendSettingsMap(c *middleware.Context) (map[string]interface{}, erro defaultDatasource = ds.Name } + if len(ds.JsonData) > 0 { + dsMap["jsonData"] = ds.JsonData + } + if ds.Access == m.DS_ACCESS_DIRECT { if ds.BasicAuth { dsMap["basicAuth"] = util.GetBasicAuthHeader(ds.BasicAuthUser, ds.BasicAuthPassword) From 9bedd83f396f6e4423006d9ded39cd031930c2cc Mon Sep 17 00:00:00 2001 From: Felix Rabe Date: Thu, 21 May 2015 00:13:50 +0200 Subject: [PATCH 29/89] Fix latest.json The trailing comma trips up both Python and JS JSON parsers. --- latest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/latest.json b/latest.json index 4d3c471727e..a85d79df539 100644 --- a/latest.json +++ b/latest.json @@ -1,3 +1,3 @@ { - "version": "2.0.2", + "version": "2.0.2" } From ba3f6f9d3e12882f3df96e320f118253c90b0776 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Thu, 21 May 2015 10:20:49 +0200 Subject: [PATCH 30/89] InfluxDB 09 fix, do not treat empty results as an error --- public/app/plugins/datasource/influxdb/datasource.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/public/app/plugins/datasource/influxdb/datasource.js b/public/app/plugins/datasource/influxdb/datasource.js index ffed5e0c520..2ff0f8beb0e 100644 --- a/public/app/plugins/datasource/influxdb/datasource.js +++ b/public/app/plugins/datasource/influxdb/datasource.js @@ -43,7 +43,6 @@ function (angular, _, kbn, InfluxSeries, InfluxQueryBuilder) { // build query var queryBuilder = new InfluxQueryBuilder(target); var query = queryBuilder.build(); - console.log('query builder result:' + query); // replace grafana variables query = query.replace('$timeFilter', timeFilter); @@ -173,7 +172,7 @@ function (angular, _, kbn, InfluxSeries, InfluxQueryBuilder) { function handleInfluxQueryResponse(alias, data) { if (!data || !data.results || !data.results[0].series) { - throw { message: 'No results in response from InfluxDB' }; + return []; } return new InfluxSeries({ series: data.results[0].series, alias: alias }).getTimeSeries(); } From 1821809e69d9339672e9d8a58e702f83a9e384e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristian=20K=C3=B6hler?= Date: Thu, 21 May 2015 12:46:26 +0200 Subject: [PATCH 31/89] listing of all none admin methods --- docs/sources/reference/http_api.md | 204 ++++++++++++++++++++++++++++- 1 file changed, 203 insertions(+), 1 deletion(-) diff --git a/docs/sources/reference/http_api.md b/docs/sources/reference/http_api.md index c0b63050199..3f5fa6392f9 100644 --- a/docs/sources/reference/http_api.md +++ b/docs/sources/reference/http_api.md @@ -141,12 +141,214 @@ Will return the dashboard given the dashboard slug. Slug is the url friendly ver The above will delete the dashboard with the specified slug. The slug is the url friendly (unique) version of the dashboard title. +### Gets the home dashboard + +`GET /api/dashboards/home` + +### Tags for Dashboard + +`GET /api/dashboards/tags` + +### Dashboard from JSON file + +`GET /file/:file` + +### Search Dashboards + +`GET /api/search/` + +Status Codes: + +- **query** – Search Query +- **tags** – Tags to use +- **starred** – Flag indicating if only starred Dashboards should be returned +- **tagcloud** - Flag indicating if a tagcloud should be returned + +**Example Request**: + + GET /api/search?query=MyDashboard&starred=true&tag=prod HTTP/1.1 + Accept: application/json + Content-Type: application/json + Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk + ## Data sources +### Get all datasources + +`GET /api/datasources` + +### Get a single data sources by Id + +`GET /api/datasources/:datasourceId` + ### Create data source -## Organizations +`PUT /api/datasources` + +**Example Response**: + + HTTP/1.1 200 + Content-Type: application/json + + {"message":"Datasource added"} + +### Edit an existing data source + +`POST /api/datasources` + +### Delete an existing data source + +`DELETE /api/datasources/:datasourceId` + +**Example Response**: + + HTTP/1.1 200 + Content-Type: application/json + + {"message":"Data source deleted"} + +### Available data source types + +`GET /api/datasources/plugins` + +## Data source proxy calls + +`GET /api/datasources/proxy/:datasourceId/*` + +Proxies all calls to the actual datasource. + +## Organisation + +### Get current Organisation + +`GET /api/org` + +### Get all users within the actual organisation + +`GET /api/org/users` + +### Add a new user to the actual organisation + +`POST /api/org/users` + +Adds a global user to the actual organisation. + +### Updates the given user + +`PATCH /api/org/users/:userId` + +### Delete user in actual organisation + +`DELETE /api/org/users/:userId` + +### Get all Users + +`GET /api/org/users` + +## Organisations + +### Search all Organisations + +`GET /api/orgs` + +### Update Organisation + +`PUT /api/orgs/:orgId` + +### Get Users in Organisation + +`GET /api/orgs/:orgId/users` + +### Add User in Organisation + +`POST /api/orgs/:orgId/users` + +### Update Users in Organisation + +`PATCH /api/orgs/:orgId/users/:userId` + +### Delete User in Organisation + +`DELETE /api/orgs/:orgId/users/:userId` ## Users +### Search Users +`GET /api/users` + +### Get single user by Id + +`GET /api/users/:id` + +### User Update + +`PUT /api/users/:id` + +### Get Organisations for user + +`GET /api/users/:id/orgs` + +## User + +### Change Password + +`PUT /api/user/password` + +Changes the password for the user + +### Actual User + +`GET /api/user` + +The above will return the current user. + +### Switch user context + +`POST /api/user/using/:organisationId` + +Switch user context to the given organisation. + +### Organisations of the actual User + +`GET /api/user/orgs` + +The above will return a list of all organisations of the current user. + +### Star a dashboard + +`POST /api/user/stars/dashboard/:dashboardId` + +Stars the given Dashboard for the actual user. + +### Unstar a dashboard + +`DELETE /api/user/stars/dashboard/:dashboardId` + +Deletes the staring of the given Dashboard for the actual user. + +## Snapshots + +### Create new snapshot + +`POST /api/snapshots` + +### Get Snapshot by Id + +`GET /api/snapshots/:key` + +### Delete Snapshot by Id + +`DELETE /api/snapshots-delete/:key` + +## Frontend Settings + +### Get Settings + +`GET /api/frontend/settings` + +## Login + +### Renew session based on remember cookie + +`GET /api/login/ping` \ No newline at end of file From 3c6b647398ac8ba49259316f7965889ea053aa5c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristian=20K=C3=B6hler?= Date: Thu, 21 May 2015 12:56:30 +0200 Subject: [PATCH 32/89] added admin stuff --- docs/sources/reference/http_api.md | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/docs/sources/reference/http_api.md b/docs/sources/reference/http_api.md index 3f5fa6392f9..d888071bd35 100644 --- a/docs/sources/reference/http_api.md +++ b/docs/sources/reference/http_api.md @@ -351,4 +351,26 @@ Deletes the staring of the given Dashboard for the actual user. ### Renew session based on remember cookie -`GET /api/login/ping` \ No newline at end of file +`GET /api/login/ping` + +## Admin + +### Settings + +`GET /api/admin/settings` + +### Global Users + +`POST /api/admin/users` + +### Password for User + +`PUT /api/admin/users/:id/password` + +### Permissions + +`PUT /api/admin/users/:id/permissions` + +### Delete global User + +`DELETE /api/admin/users/:id` From 2191921690ff89fd1680bbac742133b439d62213 Mon Sep 17 00:00:00 2001 From: David Raifaizen Date: Thu, 21 May 2015 14:36:35 -0400 Subject: [PATCH 33/89] Fixed variable name for annotations --- public/app/plugins/datasource/influxdb/influxSeries.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/app/plugins/datasource/influxdb/influxSeries.js b/public/app/plugins/datasource/influxdb/influxSeries.js index 45e4325c016..949ef88efeb 100644 --- a/public/app/plugins/datasource/influxdb/influxSeries.js +++ b/public/app/plugins/datasource/influxdb/influxSeries.js @@ -48,7 +48,7 @@ function (_) { var list = []; var self = this; - _.each(this.seriesList, function (series) { + _.each(this.series, function (series) { var titleCol = null; var timeCol = null; var tagsCol = null; From 3dc2a114fadc57e21bcf17fd0e7468255d34d978 Mon Sep 17 00:00:00 2001 From: robert jakub Date: Fri, 22 May 2015 17:08:10 +0200 Subject: [PATCH 34/89] add pps (packet per second) format --- public/app/components/kbn.js | 2 ++ public/app/panels/graph/graph.js | 3 +++ 2 files changed, 5 insertions(+) diff --git a/public/app/components/kbn.js b/public/app/components/kbn.js index 15a4c7637c3..c9d2900f53b 100644 --- a/public/app/components/kbn.js +++ b/public/app/components/kbn.js @@ -383,6 +383,7 @@ function($, _, moment) { kbn.valueFormats.mbytes = kbn.formatFuncCreator(1024, [' MiB', ' GiB', ' TiB', ' PiB', ' EiB', ' ZiB', ' YiB']); kbn.valueFormats.gbytes = kbn.formatFuncCreator(1024, [' GiB', ' TiB', ' PiB', ' EiB', ' ZiB', ' YiB']); kbn.valueFormats.bps = kbn.formatFuncCreator(1000, [' bps', ' Kbps', ' Mbps', ' Gbps', ' Tbps', ' Pbps', ' Ebps', ' Zbps', ' Ybps']); + kbn.valueFormats.pps = kbn.formatFuncCreator(1000, [' pps', ' Kpps', ' Mpps', ' Gpps', ' Tpps', ' Ppps', ' Epps', ' Zpps', ' Ypps']); kbn.valueFormats.Bps = kbn.formatFuncCreator(1000, [' Bps', ' KBps', ' MBps', ' GBps', ' TBps', ' PBps', ' EBps', ' ZBps', ' YBps']); kbn.valueFormats.short = kbn.formatFuncCreator(1000, ['', ' K', ' Mil', ' Bil', ' Tri', ' Quadr', ' Quint', ' Sext', ' Sept']); kbn.valueFormats.joule = kbn.formatFuncCreator(1000, [' J', ' kJ', ' MJ', ' GJ', ' TJ', ' PJ', ' EJ', ' ZJ', ' YJ']); @@ -564,6 +565,7 @@ function($, _, moment) { { text: 'data rate', submenu: [ + {text: 'packets/sec', value: 'pps'}, {text: 'bits/sec', value: 'bps'}, {text: 'bytes/sec', value: 'Bps'}, ] diff --git a/public/app/panels/graph/graph.js b/public/app/panels/graph/graph.js index e090ce4e85e..c6ca8ec7227 100755 --- a/public/app/panels/graph/graph.js +++ b/public/app/panels/graph/graph.js @@ -480,6 +480,9 @@ function (angular, $, kbn, moment, _, GraphTooltip) { case 'bps': url += '&yUnitSystem=si'; break; + case 'pps': + url += '&yUnitSystem=si'; + break; case 'Bps': url += '&yUnitSystem=si'; break; From b55d9350e7518462e1424fe97f355b6e1409d757 Mon Sep 17 00:00:00 2001 From: Indrek Juhkam Date: Sat, 23 May 2015 17:06:51 +0300 Subject: [PATCH 35/89] Add github organizations support --- conf/defaults.ini | 1 + conf/sample.ini | 3 +- pkg/api/login_oauth.go | 2 + pkg/social/social.go | 135 ++++++++++++++++++++++++++++++++--------- 4 files changed, 112 insertions(+), 29 deletions(-) diff --git a/conf/defaults.ini b/conf/defaults.ini index 628df5360e4..258a0198155 100644 --- a/conf/defaults.ini +++ b/conf/defaults.ini @@ -153,6 +153,7 @@ token_url = https://github.com/login/oauth/access_token api_url = https://api.github.com/user team_ids = allowed_domains = +allowed_organizations = #################################### Google Auth ########################## [auth.google] diff --git a/conf/sample.ini b/conf/sample.ini index df204a7f45d..3c2773fa674 100644 --- a/conf/sample.ini +++ b/conf/sample.ini @@ -146,12 +146,13 @@ ;allow_sign_up = false ;client_id = some_id ;client_secret = some_secret -;scopes = user:email +;scopes = user:email,read:org ;auth_url = https://github.com/login/oauth/authorize ;token_url = https://github.com/login/oauth/access_token ;api_url = https://api.github.com/user ;team_ids = ;allowed_domains = +;allowed_organizations = #################################### Google Auth ########################## [auth.google] diff --git a/pkg/api/login_oauth.go b/pkg/api/login_oauth.go index 505c17ddde8..796599df864 100644 --- a/pkg/api/login_oauth.go +++ b/pkg/api/login_oauth.go @@ -48,6 +48,8 @@ func OAuthLogin(ctx *middleware.Context) { if err != nil { if err == social.ErrMissingTeamMembership { ctx.Redirect(setting.AppSubUrl + "/login?failedMsg=" + url.QueryEscape("Required Github team membership not fulfilled")) + } else if err == social.ErrMissingOrganizationMembership { + ctx.Redirect(setting.AppSubUrl + "/login?failedMsg=" + url.QueryEscape("Required Github organization membership not fulfilled")) } else { ctx.Handle(500, fmt.Sprintf("login.OAuthLogin(get info from %s)", name), err) } diff --git a/pkg/social/social.go b/pkg/social/social.go index 355f85b54b6..49812ddd87f 100644 --- a/pkg/social/social.go +++ b/pkg/social/social.go @@ -78,12 +78,14 @@ func NewOAuthService() { if name == "github" { setting.OAuthService.GitHub = true teamIds := sec.Key("team_ids").Ints(",") + allowedOrganizations := sec.Key("allowed_organizations").Strings(" ") SocialMap["github"] = &SocialGithub{ - Config: &config, - allowedDomains: info.AllowedDomains, - apiUrl: info.ApiUrl, - allowSignup: info.AllowSignup, - teamIds: teamIds, + Config: &config, + allowedDomains: info.AllowedDomains, + apiUrl: info.ApiUrl, + allowSignup: info.AllowSignup, + teamIds: teamIds, + allowedOrganizations: allowedOrganizations, } } @@ -115,16 +117,21 @@ func isEmailAllowed(email string, allowedDomains []string) bool { type SocialGithub struct { *oauth2.Config - allowedDomains []string - apiUrl string - allowSignup bool - teamIds []int + allowedDomains []string + allowedOrganizations []string + apiUrl string + allowSignup bool + teamIds []int } var ( ErrMissingTeamMembership = errors.New("User not a member of one of the required teams") ) +var ( + ErrMissingOrganizationMembership = errors.New("User not a member of one of the required organizations") +) + func (s *SocialGithub) Type() int { return int(models.GITHUB) } @@ -137,26 +144,100 @@ func (s *SocialGithub) IsSignupAllowed() bool { return s.allowSignup } -func (s *SocialGithub) IsTeamMember(client *http.Client, username string, teamId int) bool { - var data struct { - Url string `json:"url"` - State string `json:"state"` +func (s *SocialGithub) IsTeamMember(client *http.Client) bool { + if len(s.teamIds) == 0 { + return true } - membershipUrl := fmt.Sprintf("https://api.github.com/teams/%d/memberships/%s", teamId, username) - r, err := client.Get(membershipUrl) + teamMemberships, err := s.FetchTeamMemberships(client) if err != nil { return false } - defer r.Body.Close() + for _, teamId := range s.teamIds { + for _, membershipId := range teamMemberships { + if teamId == membershipId { + return true + } + } + } - if err = json.NewDecoder(r.Body).Decode(&data); err != nil { + return false +} + +func (s *SocialGithub) IsOrganizationMember(client *http.Client) bool { + if len(s.allowedOrganizations) == 0 { + return true + } + + organizations, err := s.FetchOrganizations(client) + if err != nil { return false } - active := data.State == "active" - return active + for _, allowedOrganization := range s.allowedOrganizations { + for _, organization := range organizations { + if organization == allowedOrganization { + return true + } + } + } + + return false +} + +func (s *SocialGithub) FetchTeamMemberships(client *http.Client) ([]int, error) { + type Record struct { + Id int `json:"id"` + } + + membershipUrl := fmt.Sprintf("https://api.github.com/user/teams") + r, err := client.Get(membershipUrl) + if err != nil { + return nil, err + } + + defer r.Body.Close() + + var records []Record + + if err = json.NewDecoder(r.Body).Decode(&records); err != nil { + return nil, err + } + + var ids = make([]int, len(records)) + for i, record := range records { + ids[i] = record.Id + } + + return ids, nil +} + +func (s *SocialGithub) FetchOrganizations(client *http.Client) ([]string, error) { + type Record struct { + Login string `json:"login"` + } + + url := fmt.Sprintf("https://api.github.com/user/orgs") + r, err := client.Get(url) + if err != nil { + return nil, err + } + + defer r.Body.Close() + + var records []Record + + if err = json.NewDecoder(r.Body).Decode(&records); err != nil { + return nil, err + } + + var logins = make([]string, len(records)) + for i, record := range records { + logins[i] = record.Login + } + + return logins, nil } func (s *SocialGithub) UserInfo(token *oauth2.Token) (*BasicUserInfo, error) { @@ -185,17 +266,15 @@ func (s *SocialGithub) UserInfo(token *oauth2.Token) (*BasicUserInfo, error) { Email: data.Email, } - if len(s.teamIds) > 0 { - for _, teamId := range s.teamIds { - if s.IsTeamMember(client, data.Name, teamId) { - return userInfo, nil - } - } - + if !s.IsTeamMember(client) { return nil, ErrMissingTeamMembership - } else { - return userInfo, nil } + + if !s.IsOrganizationMember(client) { + return nil, ErrMissingOrganizationMembership + } + + return userInfo, nil } // ________ .__ From b2a0ae0f83e1c3d98a8f7822fabefac86b0230ab Mon Sep 17 00:00:00 2001 From: Brandon Turner Date: Mon, 25 May 2015 01:50:29 -0500 Subject: [PATCH 36/89] Render panel images with any SSL protocol This uses any available SSL protocol (instead the phantomjs default: SSLv3) to render panels to PNGs. This is useful when reverse proxing grafana and SSLv3 is disabled due to security vulnerabilities or other reasons. --- pkg/components/renderer/renderer.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/components/renderer/renderer.go b/pkg/components/renderer/renderer.go index aa9e0c92525..9d5ddd00d73 100644 --- a/pkg/components/renderer/renderer.go +++ b/pkg/components/renderer/renderer.go @@ -26,7 +26,7 @@ func RenderToPng(params *RenderOpts) (string, error) { pngPath, _ := filepath.Abs(filepath.Join(setting.ImagesDir, util.GetRandomString(20))) pngPath = pngPath + ".png" - cmd := exec.Command(binPath, "--ignore-ssl-errors=true", scriptPath, "url="+params.Url, "width="+params.Width, + cmd := exec.Command(binPath, "--ignore-ssl-errors=true", "--ssl-protocol=any", scriptPath, "url="+params.Url, "width="+params.Width, "height="+params.Height, "png="+pngPath, "cookiename="+setting.SessionOptions.CookieName, "domain="+setting.Domain, "sessionid="+params.SessionId) stdout, err := cmd.StdoutPipe() From 0047ce067dc4a194e2ac475f624cdfb4c64963e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 25 May 2015 10:23:32 +0200 Subject: [PATCH 37/89] SingleStatPanel: fix for color thresholds and value to text mapping combo, Fixes #2044 --- public/app/panels/singlestat/module.js | 13 +++++++------ public/app/partials/login.html | 4 ++-- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/public/app/panels/singlestat/module.js b/public/app/panels/singlestat/module.js index 3c698bfd553..24855634ba6 100644 --- a/public/app/panels/singlestat/module.js +++ b/public/app/panels/singlestat/module.js @@ -190,7 +190,12 @@ function (angular, app, _, TimeSeries, kbn, PanelMeta) { data.flotpairs = $scope.series[0].flotpairs; } - // first check value to text mappings + var decimalInfo = $scope.getDecimalsForValue(data.value); + var formatFunc = kbn.valueFormats[$scope.panel.format]; + data.valueFormated = formatFunc(data.value, decimalInfo.decimals, decimalInfo.scaledDecimals); + data.valueRounded = kbn.roundValue(data.value, decimalInfo.decimals); + + // check value to text mappings for(var i = 0; i < $scope.panel.valueMaps.length; i++) { var map = $scope.panel.valueMaps[i]; // special null case @@ -201,6 +206,7 @@ function (angular, app, _, TimeSeries, kbn, PanelMeta) { } continue; } + // value/number to text mapping var value = parseFloat(map.value); if (value === data.value) { @@ -212,11 +218,6 @@ function (angular, app, _, TimeSeries, kbn, PanelMeta) { if (data.value === null || data.value === void 0) { data.valueFormated = "no value"; } - - var decimalInfo = $scope.getDecimalsForValue(data.value); - var formatFunc = kbn.valueFormats[$scope.panel.format]; - data.valueFormated = formatFunc(data.value, decimalInfo.decimals, decimalInfo.scaledDecimals); - data.valueRounded = kbn.roundValue(data.value, decimalInfo.decimals); }; $scope.removeValueMap = function(map) { diff --git a/public/app/partials/login.html b/public/app/partials/login.html index 8db858dac8d..437588dd652 100644 --- a/public/app/partials/login.html +++ b/public/app/partials/login.html @@ -19,7 +19,7 @@
    -
    -
    -
    Value Groups/Tags
    -
    -
      -
    • - Tags query -
    • -
    • - -
    • -
    -
    -
    -
    -
      -
    • - Tags values query -
    • -
    • - -
    • -
    -
    -
    -
    -
      -
    • - -
    • -
    -
    -
    -
    -
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    From 494ede5bbfddc7dc143c76703c7fde0ff5c207da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Wed, 27 May 2015 13:11:32 +0200 Subject: [PATCH 44/89] Big refactoring/rewrite for how annotation tooltips are shown, also work on #1474 --- ' | 36 +++++++++++++++++ public/app/components/extend-jquery.js | 16 +++++++- public/app/directives/all.js | 1 + public/app/directives/annotationTooltip.js | 40 +++++++++++++++++++ public/app/directives/tags.js | 1 - .../features/annotations/annotationsSrv.js | 24 ++--------- public/app/features/dashboard/timeSrv.js | 2 +- public/app/partials/search.html | 2 +- .../plugins/datasource/graphite/datasource.js | 10 ++++- public/css/less/graph.less | 5 +++ public/vendor/jquery/jquery.flot.events.js | 8 ++-- 11 files changed, 114 insertions(+), 31 deletions(-) create mode 100644 ' create mode 100644 public/app/directives/annotationTooltip.js diff --git a/' b/' new file mode 100644 index 00000000000..b179a2b8b5a --- /dev/null +++ b/' @@ -0,0 +1,36 @@ +define([ + 'angular', + 'lodash' +], +function (angular) { + 'use strict'; + + angular + .module('grafana.directives') + .directive('annotationTooltip', function($sanitize, dashboardSrv) { + return { + scope: { tagColorFromName: "=" }, + link: function (scope, element) { + var title = $sanitize(scope.annoation.title); + var dashboard = dashboardSrv.getCurrent(); + var time = '' + dashboard.formatDate(scope.annotation.time) + ''; + + var tooltip = '
    '+ title + ' ' + time + '
    ' ; + + if (options.tags) { + var tags = $sanitize(options.tags); + tooltip += '' + (tags || '') + '
    '; + } + + if (options.text) { + var text = $sanitize(options.text); + tooltip += text.replace(/\n/g, '
    '); + } + + tooltip += "
    "; + } + }; + }); + +}); + diff --git a/public/app/components/extend-jquery.js b/public/app/components/extend-jquery.js index ce5c8afb1a9..3e1f6b0c054 100644 --- a/public/app/components/extend-jquery.js +++ b/public/app/components/extend-jquery.js @@ -1,5 +1,5 @@ -define(['jquery'], -function ($) { +define(['jquery', 'angular', 'lodash'], +function ($, angular, _) { 'use strict'; /** @@ -14,6 +14,7 @@ function ($) { return function (x, y, opts) { opts = $.extend(true, {}, defaults, opts); + return this.each(function () { var $tooltip = $(this), width, height; @@ -22,6 +23,17 @@ function ($) { $("#tooltip").remove(); $tooltip.appendTo(document.body); + if (opts.compile) { + angular.element(document).injector().invoke(function($compile, $rootScope) { + var tmpScope = $rootScope.$new(true); + _.extend(tmpScope, opts.scopeData); + + $compile($tooltip)(tmpScope); + tmpScope.$digest(); + //tmpScope.$destroy(); + }); + } + width = $tooltip.outerWidth(true); height = $tooltip.outerHeight(true); diff --git a/public/app/directives/all.js b/public/app/directives/all.js index 7e95b6f26dc..b92bc59ca41 100644 --- a/public/app/directives/all.js +++ b/public/app/directives/all.js @@ -17,4 +17,5 @@ define([ './dropdown.typeahead', './topnav', './giveFocus', + './annotationTooltip', ], function () {}); diff --git a/public/app/directives/annotationTooltip.js b/public/app/directives/annotationTooltip.js new file mode 100644 index 00000000000..f84df88dd26 --- /dev/null +++ b/public/app/directives/annotationTooltip.js @@ -0,0 +1,40 @@ +define([ + 'angular', + 'jquery', + 'lodash' +], +function (angular, $) { + 'use strict'; + + angular + .module('grafana.directives') + .directive('annotationTooltip', function($sanitize, dashboardSrv, $compile) { + return { + link: function (scope, element) { + var event = scope.event; + var title = $sanitize(event.title); + var dashboard = dashboardSrv.getCurrent(); + var time = '' + dashboard.formatDate(event.min) + ''; + + var tooltip = '
    ' + title + ' ' + time + '
    ' ; + + if (event.text) { + var text = $sanitize(event.text); + tooltip += text.replace(/\n/g, '
    ') + '
    '; + } + + if (event.tags && event.tags.length > 0) { + tooltip += '{{tag}}
    '; + } + + tooltip += "
    "; + + var $tooltip = $(tooltip); + $tooltip.appendTo(element); + + $compile(element.contents())(scope); + } + }; + }); + +}); diff --git a/public/app/directives/tags.js b/public/app/directives/tags.js index 3f77fc6ba12..4f8825a010b 100644 --- a/public/app/directives/tags.js +++ b/public/app/directives/tags.js @@ -41,7 +41,6 @@ function (angular, $) { angular .module('grafana.directives') .directive('tagColorFromName', function() { - return { scope: { tagColorFromName: "=" }, link: function (scope, element) { diff --git a/public/app/features/annotations/annotationsSrv.js b/public/app/features/annotations/annotationsSrv.js index 0ba30f1ef8b..a4529de2019 100644 --- a/public/app/features/annotations/annotationsSrv.js +++ b/public/app/features/annotations/annotationsSrv.js @@ -7,7 +7,7 @@ define([ var module = angular.module('grafana.services'); - module.service('annotationsSrv', function(datasourceSrv, $q, alertSrv, $rootScope, $sanitize) { + module.service('annotationsSrv', function(datasourceSrv, $q, alertSrv, $rootScope) { var promiseCached; var list = []; var self = this; @@ -57,30 +57,14 @@ define([ }; this.addAnnotation = function(options) { - var title = $sanitize(options.title); - var time = '' + self.dashboard.formatDate(options.time) + ''; - - var tooltip = '
    '+ title + ' ' + time + '
    ' ; - - if (options.tags) { - var tags = $sanitize(options.tags); - tooltip += '' + (tags || '') + '
    '; - } - - if (options.text) { - var text = $sanitize(options.text); - tooltip += text.replace(/\n/g, '
    '); - } - - tooltip += ""; - list.push({ annotation: options.annotation, min: options.time, max: options.time, eventType: options.annotation.name, - title: null, - description: tooltip, + title: options.title, + tags: options.tags, + text: options.text, score: 1 }); }; diff --git a/public/app/features/dashboard/timeSrv.js b/public/app/features/dashboard/timeSrv.js index 7df83f7fcab..6bb9ccde223 100644 --- a/public/app/features/dashboard/timeSrv.js +++ b/public/app/features/dashboard/timeSrv.js @@ -93,7 +93,7 @@ define([ _.extend(this.time, time); // disable refresh if we have an absolute time - if (time.to && time.to.indexOf('now') === -1) { + if (_.isString(time.to) && time.to.indexOf('now') === -1) { this.old_refresh = this.dashboard.refresh || this.old_refresh; this.set_interval(false); } diff --git a/public/app/partials/search.html b/public/app/partials/search.html index 18754010f0d..9c644e96d25 100644 --- a/public/app/partials/search.html +++ b/public/app/partials/search.html @@ -16,7 +16,7 @@ tags - | + | {{query.tag}} diff --git a/public/app/plugins/datasource/graphite/datasource.js b/public/app/plugins/datasource/graphite/datasource.js index dfd096cdb0a..3697cade07b 100644 --- a/public/app/plugins/datasource/graphite/datasource.js +++ b/public/app/plugins/datasource/graphite/datasource.js @@ -111,11 +111,19 @@ function (angular, _, $, config, kbn, moment) { var list = []; for (var i = 0; i < results.data.length; i++) { var e = results.data[i]; + var tags = []; + if (e.tags) { + tags = e.tags.split(','); + if (tags.length === 1) { + tags = e.tags.split(' '); + } + } + list.push({ annotation: annotation, time: e.when * 1000, title: e.what, - tags: e.tags, + tags: tags, text: e.data }); } diff --git a/public/css/less/graph.less b/public/css/less/graph.less index b96e466eea1..a0350d5d16c 100644 --- a/public/css/less/graph.less +++ b/public/css/less/graph.less @@ -212,6 +212,11 @@ top: -3px; } + .label-tag { + margin-right: 4px; + margin-top: 8px; + } + .graph-tooltip-list-item { display: table-row; } diff --git a/public/vendor/jquery/jquery.flot.events.js b/public/vendor/jquery/jquery.flot.events.js index 4924b16d03b..48ebf2b67fd 100644 --- a/public/vendor/jquery/jquery.flot.events.js +++ b/public/vendor/jquery/jquery.flot.events.js @@ -191,14 +191,12 @@ console.log(tooltip); */ - // @rashidkpc - hack to work with our normal tooltip placer - var $tooltip = $('
    '); + // grafana addition + var $tooltip = $('
    '); if (event) { $tooltip .html(event.description) - .place_tt(x, y, { - offset: 10 - }); + .place_tt(x, y, {offset: 10, compile: true, scopeData: {event: event}}); } else { $tooltip.remove(); } From 96bd66e8119e79e3b4da1104abeb662d618a8ca1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Wed, 27 May 2015 14:30:23 +0200 Subject: [PATCH 45/89] Made the annotation tags support more cross datasource compatible --- public/app/directives/annotationTooltip.js | 15 ++++++++++++--- .../app/plugins/datasource/graphite/datasource.js | 9 +-------- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/public/app/directives/annotationTooltip.js b/public/app/directives/annotationTooltip.js index f84df88dd26..25059d08274 100644 --- a/public/app/directives/annotationTooltip.js +++ b/public/app/directives/annotationTooltip.js @@ -3,7 +3,7 @@ define([ 'jquery', 'lodash' ], -function (angular, $) { +function (angular, $, _) { 'use strict'; angular @@ -23,8 +23,17 @@ function (angular, $) { tooltip += text.replace(/\n/g, '
    ') + '
    '; } - if (event.tags && event.tags.length > 0) { - tooltip += '{{tag}}
    '; + var tags = event.tags; + if (_.isString(event.tags)) { + tags = event.tags.split(','); + if (tags.length === 1) { + tags = event.tags.split(' '); + } + } + + if (tags && tags.length) { + scope.tags = tags; + tooltip += '{{tag}}
    '; } tooltip += "
    "; diff --git a/public/app/plugins/datasource/graphite/datasource.js b/public/app/plugins/datasource/graphite/datasource.js index 3697cade07b..9315b5a5b33 100644 --- a/public/app/plugins/datasource/graphite/datasource.js +++ b/public/app/plugins/datasource/graphite/datasource.js @@ -111,19 +111,12 @@ function (angular, _, $, config, kbn, moment) { var list = []; for (var i = 0; i < results.data.length; i++) { var e = results.data[i]; - var tags = []; - if (e.tags) { - tags = e.tags.split(','); - if (tags.length === 1) { - tags = e.tags.split(' '); - } - } list.push({ annotation: annotation, time: e.when * 1000, title: e.what, - tags: tags, + tags: e.tags, text: e.data }); } From aeb8bc875584ae884adafcea3b3ab2074ded0a2c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Thu, 28 May 2015 09:35:47 +0200 Subject: [PATCH 46/89] Share link should always have absolute time range, Closes #2060 --- .../app/features/dashboard/shareModalCtrl.js | 6 ++--- public/test/specs/shareModalCtrl-specs.js | 26 +++++-------------- 2 files changed, 9 insertions(+), 23 deletions(-) diff --git a/public/app/features/dashboard/shareModalCtrl.js b/public/app/features/dashboard/shareModalCtrl.js index 1dd99dbb126..55ec0c8a410 100644 --- a/public/app/features/dashboard/shareModalCtrl.js +++ b/public/app/features/dashboard/shareModalCtrl.js @@ -43,9 +43,9 @@ function (angular, _, require, config) { var params = angular.copy($location.search()); - var range = timeSrv.timeRangeForUrl(); - params.from = range.from; - params.to = range.to; + var range = timeSrv.timeRange(); + params.from = range.from.getTime(); + params.to = range.to.getTime(); if ($scope.options.includeTemplateVars) { templateSrv.fillVariableValuesForUrl(params); diff --git a/public/test/specs/shareModalCtrl-specs.js b/public/test/specs/shareModalCtrl-specs.js index d0d69479a13..c9d5131d11a 100644 --- a/public/test/specs/shareModalCtrl-specs.js +++ b/public/test/specs/shareModalCtrl-specs.js @@ -9,10 +9,10 @@ define([ var ctx = new helpers.ControllerTestContext(); function setTime(range) { - ctx.timeSrv.timeRangeForUrl = sinon.stub().returns(range); + ctx.timeSrv.timeRange = sinon.stub().returns(range); } - setTime({ from: 'now-1h', to: 'now' }); + setTime({ from: new Date(1000), to: new Date(2000) }); beforeEach(module('grafana.controllers')); beforeEach(module('grafana.services')); @@ -23,57 +23,43 @@ define([ describe('shareUrl with current time range and panel', function() { - it('should generate share url relative time', function() { - ctx.$location.path('/test'); - ctx.scope.panel = { id: 22 }; - - setTime({ from: 'now-1h', to: 'now' }); - - ctx.scope.init(); - expect(ctx.scope.shareUrl).to.be('http://server/#/test?from=now-1h&to=now&panelId=22&fullscreen'); - }); - it('should generate share url absolute time', function() { ctx.$location.path('/test'); ctx.scope.panel = { id: 22 }; - setTime({ from: 1362178800000, to: 1396648800000 }); ctx.scope.init(); - expect(ctx.scope.shareUrl).to.be('http://server/#/test?from=1362178800000&to=1396648800000&panelId=22&fullscreen'); + expect(ctx.scope.shareUrl).to.be('http://server/#/test?from=1000&to=2000&panelId=22&fullscreen'); }); it('should remove panel id when no panel in scope', function() { ctx.$location.path('/test'); ctx.scope.options.forCurrent = true; ctx.scope.panel = null; - setTime({ from: 'now-1h', to: 'now' }); ctx.scope.init(); - expect(ctx.scope.shareUrl).to.be('http://server/#/test?from=now-1h&to=now'); + expect(ctx.scope.shareUrl).to.be('http://server/#/test?from=1000&to=2000'); }); it('should add theme when specified', function() { ctx.$location.path('/test'); ctx.scope.options.theme = 'light'; ctx.scope.panel = null; - setTime({ from: 'now-1h', to: 'now' }); ctx.scope.init(); - expect(ctx.scope.shareUrl).to.be('http://server/#/test?from=now-1h&to=now&theme=light'); + expect(ctx.scope.shareUrl).to.be('http://server/#/test?from=1000&to=2000&theme=light'); }); it('should include template variables in url', function() { ctx.$location.path('/test'); ctx.scope.options.includeTemplateVars = true; - setTime({ from: 'now-1h', to: 'now' }); ctx.templateSrv.fillVariableValuesForUrl = function(params) { params['var-app'] = 'mupp'; params['var-server'] = 'srv-01'; }; ctx.scope.buildUrl(); - expect(ctx.scope.shareUrl).to.be('http://server/#/test?from=now-1h&to=now&var-app=mupp&var-server=srv-01'); + expect(ctx.scope.shareUrl).to.be('http://server/#/test?from=1000&to=2000&var-app=mupp&var-server=srv-01'); }); }); From 57fac6b9aaf0a10b36a5f2a71582fb113da738aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Thu, 28 May 2015 11:41:32 +0200 Subject: [PATCH 47/89] Removed invalid scripted dashboard example, Closes #2063 --- public/dashboards/scripted_gen_and_save.js | 95 ---------------------- 1 file changed, 95 deletions(-) delete mode 100644 public/dashboards/scripted_gen_and_save.js diff --git a/public/dashboards/scripted_gen_and_save.js b/public/dashboards/scripted_gen_and_save.js deleted file mode 100644 index b7ad24bc25c..00000000000 --- a/public/dashboards/scripted_gen_and_save.js +++ /dev/null @@ -1,95 +0,0 @@ -/* global _ */ - -/* - * Complex scripted dashboard - * This script generates a dashboard object that Grafana can load. It also takes a number of user - * supplied URL parameters (in the 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'; - -// accessible 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); - } - }); - }); - -}; - From cf147cdeafb4e299984e0552d423bad4d7660481 Mon Sep 17 00:00:00 2001 From: Andrea Bernardo Ciddio Date: Thu, 28 May 2015 16:47:20 +0100 Subject: [PATCH 48/89] GitHub users without a public email should be authenticated using their primary private email address --- pkg/social/social.go | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/pkg/social/social.go b/pkg/social/social.go index 49812ddd87f..1a00934b937 100644 --- a/pkg/social/social.go +++ b/pkg/social/social.go @@ -186,6 +186,37 @@ func (s *SocialGithub) IsOrganizationMember(client *http.Client) bool { return false } +func (s *SocialGithub) FetchPrivateEmail(client *http.Client) (string, error) { + type Record struct { + Email string `json:"email"` + Primary bool `json:"primary"` + Verified bool `json:"verified"` + } + + emailsUrl := fmt.Sprintf("https://api.github.com/user/emails") + r, err := client.Get(emailsUrl) + if err != nil { + return "", err + } + + defer r.Body.Close() + + var records []Record + + if err = json.NewDecoder(r.Body).Decode(&records); err != nil { + return "", err + } + + var email = "" + for _, record := range records { + if record.Primary { + email = record.Email + } + } + + return email, nil +} + func (s *SocialGithub) FetchTeamMemberships(client *http.Client) ([]int, error) { type Record struct { Id int `json:"id"` @@ -274,6 +305,13 @@ func (s *SocialGithub) UserInfo(token *oauth2.Token) (*BasicUserInfo, error) { return nil, ErrMissingOrganizationMembership } + if userInfo.Email == "" { + userInfo.Email, err = s.FetchPrivateEmail(client) + if err != nil { + return nil, err + } + } + return userInfo, nil } From 0108dfa80327396545c8a00b1ddfb402602b3acc Mon Sep 17 00:00:00 2001 From: yinchuan Date: Fri, 29 May 2015 11:23:14 +0800 Subject: [PATCH 49/89] Update configuration.md --- docs/sources/installation/configuration.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/sources/installation/configuration.md b/docs/sources/installation/configuration.md index 4665803960a..6e13398566b 100644 --- a/docs/sources/installation/configuration.md +++ b/docs/sources/installation/configuration.md @@ -296,12 +296,12 @@ Secret. Specify these in the Grafana configuration file. For example: scopes = https://www.googleapis.com/auth/userinfo.profile https://www.googleapis.com/auth/userinfo.email auth_url = https://accounts.google.com/o/oauth2/auth token_url = https://accounts.google.com/o/oauth2/token - allowed_domains = mycompany.com + allowed_domains = mycompany.com mycompany.org allow_sign_up = false Restart the Grafana back-end. You should now see a Google login button on the login page. You can now login or sign up with your Google -accounts. The `allowed_domains` option is optional. +accounts. The `allowed_domains` option is optional, and domains is seperated by space. You may allow users to sign-up via Google authentication by setting the `allow_sign_up` option to `true`. When this option is set to `true`, any From ed974a808bac3d5cf5f77b387185eb3b2770e273 Mon Sep 17 00:00:00 2001 From: yinchuan Date: Fri, 29 May 2015 11:39:59 +0800 Subject: [PATCH 50/89] Update configuration.md --- docs/sources/installation/configuration.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/sources/installation/configuration.md b/docs/sources/installation/configuration.md index 6e13398566b..e27a3e80f6a 100644 --- a/docs/sources/installation/configuration.md +++ b/docs/sources/installation/configuration.md @@ -301,7 +301,7 @@ Secret. Specify these in the Grafana configuration file. For example: Restart the Grafana back-end. You should now see a Google login button on the login page. You can now login or sign up with your Google -accounts. The `allowed_domains` option is optional, and domains is seperated by space. +accounts. The `allowed_domains` option is optional, and domains were seperated by space. You may allow users to sign-up via Google authentication by setting the `allow_sign_up` option to `true`. When this option is set to `true`, any From fc43ce657c91a7bcaaeb95c1e806b60dc3c6cc28 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 1 Jun 2015 11:00:05 +0200 Subject: [PATCH 51/89] allow data source proxy to proxy requests over self signed https connections, Closes #2069 --- pkg/api/dataproxy.go | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/pkg/api/dataproxy.go b/pkg/api/dataproxy.go index 81318cdc536..11075294b66 100644 --- a/pkg/api/dataproxy.go +++ b/pkg/api/dataproxy.go @@ -1,9 +1,12 @@ package api import ( + "crypto/tls" + "net" "net/http" "net/http/httputil" "net/url" + "time" "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/middleware" @@ -11,6 +14,16 @@ import ( "github.com/grafana/grafana/pkg/util" ) +var dataProxyTransport = &http.Transport{ + TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, + Proxy: http.ProxyFromEnvironment, + Dial: (&net.Dialer{ + Timeout: 30 * time.Second, + KeepAlive: 30 * time.Second, + }).Dial, + TLSHandshakeTimeout: 10 * time.Second, +} + func NewReverseProxy(ds *m.DataSource, proxyPath string) *httputil.ReverseProxy { target, _ := url.Parse(ds.Url) @@ -56,5 +69,6 @@ func ProxyDataSourceRequest(c *middleware.Context) { proxyPath := c.Params("*") proxy := NewReverseProxy(&query.Result, proxyPath) + proxy.Transport = dataProxyTransport proxy.ServeHTTP(c.RW(), c.Req.Request) } From e2f6633d57624664654463578e8d2502bfd7ffef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 1 Jun 2015 12:15:49 +0200 Subject: [PATCH 52/89] Began work on data source test / validation, #1997 & #2043 --- CHANGELOG.md | 1 + pkg/api/api.go | 7 ++- pkg/api/datasources.go | 4 +- pkg/models/datasource.go | 2 +- public/app/features/org/datasourceEditCtrl.js | 46 +++++++++++++------ .../features/org/partials/datasourceEdit.html | 21 +++++++-- .../plugins/datasource/graphite/datasource.js | 16 +++++++ public/css/less/overrides.less | 45 +++++++++--------- 8 files changed, 95 insertions(+), 47 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ed6265738ef..f03c7f34891 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,7 @@ - [Issue #1928](https://github.com/grafana/grafana/issues/1928). HTTP API: GET /api/dashboards/db/:slug response changed property `model` to `dashboard` to match the POST request nameing - Backend render URL changed from `/render/dashboard/solo` `render/dashboard-solo/` (in order to have consistent dashboard url `/dashboard/:type/:slug`) - Search HTTP API response has changed (simplified), tags list moved to seperate HTTP resource URI +- Datasource HTTP api breaking change, ADD datasource is now POST /api/datasources/, update is now PUT /api/datasources/:id # 2.0.3 (unreleased - 2.0.x branch) diff --git a/pkg/api/api.go b/pkg/api/api.go index f5da406a8a5..6ecaa51652e 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -107,10 +107,9 @@ func Register(r *macaron.Macaron) { // Data sources r.Group("/datasources", func() { - r.Combo("/"). - Get(GetDataSources). - Put(bind(m.AddDataSourceCommand{}), AddDataSource). - Post(bind(m.UpdateDataSourceCommand{}), UpdateDataSource) + r.Get("/", GetDataSources) + r.Post("/", bind(m.AddDataSourceCommand{}), AddDataSource) + r.Put("/:id", bind(m.UpdateDataSourceCommand{}), UpdateDataSource) r.Delete("/:id", DeleteDataSource) r.Get("/:id", GetDataSourceById) r.Get("/plugins", GetDataSourcePlugins) diff --git a/pkg/api/datasources.go b/pkg/api/datasources.go index 76a9bddd253..e0253df3cdb 100644 --- a/pkg/api/datasources.go +++ b/pkg/api/datasources.go @@ -6,6 +6,7 @@ import ( "github.com/grafana/grafana/pkg/middleware" m "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/plugins" + "github.com/grafana/grafana/pkg/util" ) func GetDataSources(c *middleware.Context) { @@ -94,11 +95,12 @@ func AddDataSource(c *middleware.Context, cmd m.AddDataSourceCommand) { return } - c.JsonOK("Datasource added") + c.JSON(200, util.DynMap{"message": "Datasource added", "id": cmd.Result.Id}) } func UpdateDataSource(c *middleware.Context, cmd m.UpdateDataSourceCommand) { cmd.OrgId = c.OrgId + cmd.Id = c.ParamsInt64(":id") err := bus.Dispatch(&cmd) if err != nil { diff --git a/pkg/models/datasource.go b/pkg/models/datasource.go index 2ba236cd56b..c756faaba59 100644 --- a/pkg/models/datasource.go +++ b/pkg/models/datasource.go @@ -69,7 +69,6 @@ type AddDataSourceCommand struct { // Also acts as api DTO type UpdateDataSourceCommand struct { - Id int64 `json:"id" binding:"Required"` Name string `json:"name" binding:"Required"` Type string `json:"type" binding:"Required"` Access DsAccess `json:"access" binding:"Required"` @@ -84,6 +83,7 @@ type UpdateDataSourceCommand struct { JsonData map[string]interface{} `json:"jsonData"` OrgId int64 `json:"-"` + Id int64 `json:"-"` } type DeleteDataSourceCommand struct { diff --git a/public/app/features/org/datasourceEditCtrl.js b/public/app/features/org/datasourceEditCtrl.js index 780aee3e843..12d552c985b 100644 --- a/public/app/features/org/datasourceEditCtrl.js +++ b/public/app/features/org/datasourceEditCtrl.js @@ -25,7 +25,6 @@ function (angular, config) { $scope.loadDatasourceTypes().then(function() { if ($routeParams.id) { - $scope.isNew = false; $scope.getDatasourceById($routeParams.id); } else { $scope.current = angular.copy(defaults); @@ -48,6 +47,7 @@ function (angular, config) { $scope.getDatasourceById = function(id) { backendSrv.get('/api/datasources/' + id).then(function(ds) { + $scope.isNew = false; $scope.current = ds; $scope.typeChanged(); }); @@ -65,26 +65,46 @@ function (angular, config) { }); }; - $scope.update = function() { - if (!$scope.editForm.$valid) { - return; - } + $scope.testDatasource = function() { + $scope.testing = { done: false }; - backendSrv.post('/api/datasources', $scope.current).then(function() { - $scope.updateFrontendSettings(); - $location.path("datasources"); + datasourceSrv.get($scope.current.name).then(function(datasource) { + if (!datasource.testDatasource) { + $scope.testing.message = 'Data source does not support test connection feature.'; + $scope.testing.status = 'warning'; + $scope.testing.title = 'Unknown'; + return; + } + return datasource.testDatasource().then(function(result) { + $scope.testing.message = result.message; + $scope.testing.status = result.status; + $scope.testing.title = result.title; + }); + }).finally(function() { + $scope.testing.done = true; }); }; - $scope.add = function() { + $scope.saveChanges = function(test) { if (!$scope.editForm.$valid) { return; } - backendSrv.put('/api/datasources', $scope.current).then(function() { - $scope.updateFrontendSettings(); - $location.path("datasources"); - }); + if ($scope.current.id) { + return backendSrv.put('/api/datasources/' + $scope.current.id, $scope.current).then(function() { + $scope.updateFrontendSettings(); + if (test) { + $scope.testDatasource(); + } else { + $location.path('datasources'); + } + }); + } else { + return backendSrv.post('/api/datasources', $scope.current).then(function(result) { + $scope.updateFrontendSettings(); + $location.path('datasources/edit/' + result.id); + }); + } }; $scope.init(); diff --git a/public/app/features/org/partials/datasourceEdit.html b/public/app/features/org/partials/datasourceEdit.html index 12b46ee284b..6ea33e5a43c 100644 --- a/public/app/features/org/partials/datasourceEdit.html +++ b/public/app/features/org/partials/datasourceEdit.html @@ -43,11 +43,22 @@
    -
    -
    -
    - - + +
    +
    Testing....
    +
    Test results
    +
    +
    {{testing.title}}
    +
    +
    +
    + +
    + + + Cancel

    diff --git a/public/app/plugins/datasource/graphite/datasource.js b/public/app/plugins/datasource/graphite/datasource.js index 9315b5a5b33..ff06d9f46aa 100644 --- a/public/app/plugins/datasource/graphite/datasource.js +++ b/public/app/plugins/datasource/graphite/datasource.js @@ -196,6 +196,22 @@ function (angular, _, $, config, kbn, moment) { }); }; + GraphiteDatasource.prototype.testDatasource = function() { + return this.metricFindQuery('*').then(function () { + return { status: "success", message: "Data source is working", title: "Success" }; + }, function(err) { + var message, title; + if (err.statusText) { + message = err.statusText; + title = "HTTP Error"; + } else { + message = err; + title = "Unknown error"; + } + return { status: "error", message: message, title: title }; + }); + }; + GraphiteDatasource.prototype.listDashboards = function(query) { return this.doGraphiteRequest({ method: 'GET', url: '/dashboard/find/', params: {query: query || ''} }) .then(function(results) { diff --git a/public/css/less/overrides.less b/public/css/less/overrides.less index 425777f1597..b03cb7f7e6b 100644 --- a/public/css/less/overrides.less +++ b/public/css/less/overrides.less @@ -315,38 +315,37 @@ div.flot-text { position: fixed; right: 20px; top: 56px; +} - .alert { - color: @white; - padding-bottom: 13px; - position: relative; - } +.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-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; - } +.alert-title { + font-weight: bold; + padding-bottom: 2px; } .alert-warning { background-color: @warningBackground; border-color: @warningBorder; - color: @warningText; } /* =================================================== From afede880e6900ad192fb8ba0c9fece2e7c5f3fe9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 1 Jun 2015 14:02:24 +0200 Subject: [PATCH 53/89] added url validation when adding data source, Fixes #2043 --- .../org/partials/datasourceHttpConfig.html | 2 +- .../plugins/datasource/influxdb/datasource.js | 16 ++++++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/public/app/features/org/partials/datasourceHttpConfig.html b/public/app/features/org/partials/datasourceHttpConfig.html index 88923e77b3d..94c208d8370 100644 --- a/public/app/features/org/partials/datasourceHttpConfig.html +++ b/public/app/features/org/partials/datasourceHttpConfig.html @@ -6,7 +6,7 @@ Url
  • - +
  • Access Direct = url is used directly from browser, Proxy = Grafana backend will proxy the request diff --git a/public/app/plugins/datasource/influxdb/datasource.js b/public/app/plugins/datasource/influxdb/datasource.js index 2ff0f8beb0e..c483e28b288 100644 --- a/public/app/plugins/datasource/influxdb/datasource.js +++ b/public/app/plugins/datasource/influxdb/datasource.js @@ -126,6 +126,22 @@ function (angular, _, kbn, InfluxSeries, InfluxQueryBuilder) { return this._influxRequest('GET', '/query', {q: query}); }; + InfluxDatasource.prototype.testDatasource = function() { + return this.metricFindQuery('SHOW MEASUREMENTS LIMIT 1').then(function () { + return { status: "success", message: "Data source is working", title: "Success" }; + }, function(err) { + var message, title; + if (err.statusText) { + message = err.statusText; + title = "HTTP Error"; + } else { + message = err; + title = "Unknown error"; + } + return { status: "error", message: message, title: title }; + }); + }; + InfluxDatasource.prototype._influxRequest = function(method, url, data) { var self = this; var deferred = $q.defer(); From 50645cc36bfdec608c12f36a2acfc610e5b08df7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 1 Jun 2015 14:16:59 +0200 Subject: [PATCH 54/89] Added test connection action to all data sources, Closes #1997 --- public/app/features/org/datasourceEditCtrl.js | 9 +++++++++ public/app/plugins/datasource/graphite/datasource.js | 10 ---------- public/app/plugins/datasource/influxdb/datasource.js | 10 ---------- .../app/plugins/datasource/influxdb_08/datasource.js | 6 ++++++ public/app/plugins/datasource/opentsdb/datasource.js | 6 ++++++ 5 files changed, 21 insertions(+), 20 deletions(-) diff --git a/public/app/features/org/datasourceEditCtrl.js b/public/app/features/org/datasourceEditCtrl.js index 12d552c985b..f2af6325a20 100644 --- a/public/app/features/org/datasourceEditCtrl.js +++ b/public/app/features/org/datasourceEditCtrl.js @@ -75,10 +75,19 @@ function (angular, config) { $scope.testing.title = 'Unknown'; return; } + return datasource.testDatasource().then(function(result) { $scope.testing.message = result.message; $scope.testing.status = result.status; $scope.testing.title = result.title; + }, function(err) { + if (err.statusText) { + $scope.testing.message = err.statusText; + $scope.testing.title = "HTTP Error"; + } else { + $scope.testing.message = err.message; + $scope.testing.title = "Unknown error"; + } }); }).finally(function() { $scope.testing.done = true; diff --git a/public/app/plugins/datasource/graphite/datasource.js b/public/app/plugins/datasource/graphite/datasource.js index ff06d9f46aa..a3255584879 100644 --- a/public/app/plugins/datasource/graphite/datasource.js +++ b/public/app/plugins/datasource/graphite/datasource.js @@ -199,16 +199,6 @@ function (angular, _, $, config, kbn, moment) { GraphiteDatasource.prototype.testDatasource = function() { return this.metricFindQuery('*').then(function () { return { status: "success", message: "Data source is working", title: "Success" }; - }, function(err) { - var message, title; - if (err.statusText) { - message = err.statusText; - title = "HTTP Error"; - } else { - message = err; - title = "Unknown error"; - } - return { status: "error", message: message, title: title }; }); }; diff --git a/public/app/plugins/datasource/influxdb/datasource.js b/public/app/plugins/datasource/influxdb/datasource.js index c483e28b288..a8656114fdf 100644 --- a/public/app/plugins/datasource/influxdb/datasource.js +++ b/public/app/plugins/datasource/influxdb/datasource.js @@ -129,16 +129,6 @@ function (angular, _, kbn, InfluxSeries, InfluxQueryBuilder) { InfluxDatasource.prototype.testDatasource = function() { return this.metricFindQuery('SHOW MEASUREMENTS LIMIT 1').then(function () { return { status: "success", message: "Data source is working", title: "Success" }; - }, function(err) { - var message, title; - if (err.statusText) { - message = err.statusText; - title = "HTTP Error"; - } else { - message = err; - title = "Unknown error"; - } - return { status: "error", message: message, title: title }; }); }; diff --git a/public/app/plugins/datasource/influxdb_08/datasource.js b/public/app/plugins/datasource/influxdb_08/datasource.js index 8ff01c8553f..0e4adba072c 100644 --- a/public/app/plugins/datasource/influxdb_08/datasource.js +++ b/public/app/plugins/datasource/influxdb_08/datasource.js @@ -99,6 +99,12 @@ function (angular, _, kbn, InfluxSeries, InfluxQueryBuilder) { }); }; + InfluxDatasource.prototype.testDatasource = function() { + return this.metricFindQuery('list series').then(function () { + return { status: "success", message: "Data source is working", title: "Success" }; + }); + }; + InfluxDatasource.prototype.metricFindQuery = function (query) { var interpolated; try { diff --git a/public/app/plugins/datasource/opentsdb/datasource.js b/public/app/plugins/datasource/opentsdb/datasource.js index 936500be0f0..1e60bf1e54e 100644 --- a/public/app/plugins/datasource/opentsdb/datasource.js +++ b/public/app/plugins/datasource/opentsdb/datasource.js @@ -90,6 +90,12 @@ function (angular, _, kbn) { }); }; + OpenTSDBDatasource.prototype.testDatasource = function() { + return this.performSuggestQuery('cpu', 'metrics').then(function () { + return { status: "success", message: "Data source is working", title: "Success" }; + }); + }; + function transformMetricData(md, groupByTags, options) { var metricLabel = createMetricLabel(md, options, groupByTags); var dps = []; From 85c3a0aa1441bd37c7fc455ab51effbc7bf53741 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 1 Jun 2015 16:29:45 +0200 Subject: [PATCH 55/89] Panel menu now hides edit actions for users with role Viewer, Closes #1826 --- ' | 36 ------------- pkg/api/dashboard.go | 1 + pkg/api/dtos/models.go | 1 + pkg/api/frontendsettings.go | 1 + pkg/setting/setting.go | 2 + public/app/components/panelmeta.js | 12 ++--- public/app/features/panel/panelMenu.js | 70 +++++++++++++++----------- public/app/services/contextSrv.js | 2 +- public/css/less/panel.less | 1 - 9 files changed, 54 insertions(+), 72 deletions(-) delete mode 100644 ' diff --git a/' b/' deleted file mode 100644 index b179a2b8b5a..00000000000 --- a/' +++ /dev/null @@ -1,36 +0,0 @@ -define([ - 'angular', - 'lodash' -], -function (angular) { - 'use strict'; - - angular - .module('grafana.directives') - .directive('annotationTooltip', function($sanitize, dashboardSrv) { - return { - scope: { tagColorFromName: "=" }, - link: function (scope, element) { - var title = $sanitize(scope.annoation.title); - var dashboard = dashboardSrv.getCurrent(); - var time = '' + dashboard.formatDate(scope.annotation.time) + ''; - - var tooltip = '
    '+ title + ' ' + time + '
    ' ; - - if (options.tags) { - var tags = $sanitize(options.tags); - tooltip += '' + (tags || '') + '
    '; - } - - if (options.text) { - var text = $sanitize(options.text); - tooltip += text.replace(/\n/g, '
    '); - } - - tooltip += ""; - } - }; - }); - -}); - diff --git a/pkg/api/dashboard.go b/pkg/api/dashboard.go index b439bd67ac7..00ae26744d9 100644 --- a/pkg/api/dashboard.go +++ b/pkg/api/dashboard.go @@ -55,6 +55,7 @@ func GetDashboard(c *middleware.Context) { Type: m.DashTypeDB, CanStar: c.IsSignedIn, CanSave: c.OrgRole == m.ROLE_ADMIN || c.OrgRole == m.ROLE_EDITOR, + CanEdit: c.OrgRole == m.ROLE_ADMIN || c.OrgRole == m.ROLE_EDITOR, }, } diff --git a/pkg/api/dtos/models.go b/pkg/api/dtos/models.go index d5e294cc983..3e1826f56fb 100644 --- a/pkg/api/dtos/models.go +++ b/pkg/api/dtos/models.go @@ -34,6 +34,7 @@ type DashboardMeta struct { IsSnapshot bool `json:"isSnapshot,omitempty"` Type string `json:"type,omitempty"` CanSave bool `json:"canSave"` + CanEdit bool `json:"canEdit"` CanStar bool `json:"canStar"` Slug string `json:"slug"` Expires time.Time `json:"expires"` diff --git a/pkg/api/frontendsettings.go b/pkg/api/frontendsettings.go index ea154320608..4dd6ba06819 100644 --- a/pkg/api/frontendsettings.go +++ b/pkg/api/frontendsettings.go @@ -99,6 +99,7 @@ func getFrontendSettingsMap(c *middleware.Context) (map[string]interface{}, erro "defaultDatasource": defaultDatasource, "datasources": datasources, "appSubUrl": setting.AppSubUrl, + "viewerRoleMode": setting.ViewerRoleMode, "buildInfo": map[string]interface{}{ "version": setting.BuildVersion, "commit": setting.BuildCommit, diff --git a/pkg/setting/setting.go b/pkg/setting/setting.go index 77445dd6c4c..6768f9aabd9 100644 --- a/pkg/setting/setting.go +++ b/pkg/setting/setting.go @@ -79,6 +79,7 @@ var ( AllowUserOrgCreate bool AutoAssignOrg bool AutoAssignOrgRole string + ViewerRoleMode string // Http auth AdminUser string @@ -383,6 +384,7 @@ func NewConfigContext(args *CommandLineArgs) { AllowUserOrgCreate = users.Key("allow_org_create").MustBool(true) AutoAssignOrg = users.Key("auto_assign_org").MustBool(true) AutoAssignOrgRole = users.Key("auto_assign_org_role").In("Editor", []string{"Editor", "Admin", "Viewer"}) + ViewerRoleMode = users.Key("viewer_role_mode").In("default", []string{"default", "strinct"}) // anonymous access AnonymousEnabled = Cfg.Section("auth.anonymous").Key("enabled").MustBool(false) diff --git a/public/app/components/panelmeta.js b/public/app/components/panelmeta.js index 4ee4a9b9b55..013919c174a 100644 --- a/public/app/components/panelmeta.js +++ b/public/app/components/panelmeta.js @@ -16,8 +16,8 @@ function () { this.addMenuItem('view', 'icon-eye-open', 'toggleFullscreen(false); dismiss();'); } - this.addMenuItem('edit', 'icon-cog', 'editPanel(); dismiss();'); - this.addMenuItem('duplicate', 'icon-copy', 'duplicatePanel()'); + this.addMenuItem('edit', 'icon-cog', 'editPanel(); dismiss();', 'Editor'); + this.addMenuItem('duplicate', 'icon-copy', 'duplicatePanel()', 'Editor'); this.addMenuItem('share', 'icon-share', 'sharePanel(); dismiss();'); this.addEditorTab('General', 'app/partials/panelgeneral.html'); @@ -29,12 +29,12 @@ function () { this.addExtendedMenuItem('Panel JSON', '', 'editPanelJson(); dismiss();'); } - PanelMeta.prototype.addMenuItem = function(text, icon, click) { - this.menu.push({text: text, icon: icon, click: click}); + PanelMeta.prototype.addMenuItem = function(text, icon, click, role) { + this.menu.push({text: text, icon: icon, click: click, role: role}); }; - PanelMeta.prototype.addExtendedMenuItem = function(text, icon, click) { - this.extendedMenu.push({text: text, icon: icon, click: click}); + PanelMeta.prototype.addExtendedMenuItem = function(text, icon, click, role) { + this.extendedMenu.push({text: text, icon: icon, click: click, role: role}); }; PanelMeta.prototype.addEditorTab = function(title, src) { diff --git a/public/app/features/panel/panelMenu.js b/public/app/features/panel/panelMenu.js index 27152ef2b94..03ee78491ab 100644 --- a/public/app/features/panel/panelMenu.js +++ b/public/app/features/panel/panelMenu.js @@ -8,7 +8,7 @@ function (angular, $, _) { angular .module('grafana.directives') - .directive('panelMenu', function($compile, linkSrv) { + .directive('panelMenu', function($compile, linkSrv, contextSrv) { var linkTemplate = '' + '{{panel.title | interpolateTemplateVars:this}}' + @@ -18,18 +18,26 @@ function (angular, $, _) { function createMenuTemplate($scope) { var template = '
    '; - template += '
    '; - template += '
    '; - template += ''; - template += ''; - template += ''; - template += '
    '; - template += '
    '; + + if ($scope.dashboardMeta.canEdit && contextSrv.isEditor) { + template += '
    '; + template += '
    '; + template += ''; + template += ''; + template += ''; + template += '
    '; + template += '
    '; + } template += '
    '; template += ''; _.each($scope.panelMeta.menu, function(item) { + // skip edit actions if not editor + if (item.role === 'Editor' && (!contextSrv.isEditor || !$scope.dashboardMeta.canEdit)) { + return; + } + template += ' 0) { - menuLeftPos -= stickingOut + 10; - } - if (panelLeftPos + menuLeftPos < 0) { - menuLeftPos = 0; - } - var menuTemplate = createMenuTemplate($scope); $menu = $(menuTemplate); - $menu.css('left', menuLeftPos); $menu.mouseleave(function() { dismiss(1000); }); @@ -136,15 +130,35 @@ function (angular, $, _) { dismiss(null, true); }; - $('.panel-menu').remove(); - elem.append($menu); - $scope.$apply(function() { - $compile($menu.contents())(menuScope); - }); - $(".panel-container").removeClass('panel-highlight'); $panelContainer.toggleClass('panel-highlight'); + $('.panel-menu').remove(); + + elem.append($menu); + + $scope.$apply(function() { + $compile($menu.contents())(menuScope); + + var menuWidth = $menu[0].offsetWidth; + var menuHeight = $menu[0].offsetHeight; + + var windowWidth = $(window).width(); + var panelLeftPos = $(elem).offset().left; + var panelWidth = $(elem).width(); + + var menuLeftPos = (panelWidth / 2) - (menuWidth/2); + var stickingOut = panelLeftPos + menuLeftPos + menuWidth - windowWidth; + if (stickingOut > 0) { + menuLeftPos -= stickingOut + 10; + } + if (panelLeftPos + menuLeftPos < 0) { + menuLeftPos = 0; + } + + $menu.css({'left': menuLeftPos, top: -menuHeight}); + }); + dismiss(2200); }; diff --git a/public/app/services/contextSrv.js b/public/app/services/contextSrv.js index b3f8a1ed164..aa844ee5113 100644 --- a/public/app/services/contextSrv.js +++ b/public/app/services/contextSrv.js @@ -60,6 +60,6 @@ function (angular, _, store, config) { store.set('grafana.sidemenu', false); } - this.isEditor = this.hasRole('Editor') || this.hasRole('Admin'); + this.isEditor = this.hasRole('Editor') || this.hasRole('Admin') || this.hasRole('Read Only Editor'); }); }); diff --git a/public/css/less/panel.less b/public/css/less/panel.less index 17b8ff0cd52..6f011b2978f 100644 --- a/public/css/less/panel.less +++ b/public/css/less/panel.less @@ -130,7 +130,6 @@ position: absolute; background: @grafanaTargetFuncBackground; border: 1px solid black; - top: -62px; .panel-menu-row { white-space: nowrap; From 83e7c48767ce8f07d4e53e05af9de4ae705cfa9b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 1 Jun 2015 16:39:01 +0200 Subject: [PATCH 56/89] User role 'Viewer' are now prohibited from entering edit mode (and doing other transient dashboard edits). A new role will replace the old Viewer behavior --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f03c7f34891..bc4c86878d4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ - [Issue #960](https://github.com/grafana/grafana/issues/960). Search: Backend can now index a folder with json files, will be available in search (saving back to folder is not supported, this feature is meant for static generated json dashboards) **Breaking changes** +- [Issue #1826](https://github.com/grafana/grafana/issues/1826). User role 'Viewer' are now prohibited from entering edit mode (and doing other transient dashboard edits). A new role `Read Only Editor` will replace the old Viewer behavior - [Issue #1928](https://github.com/grafana/grafana/issues/1928). HTTP API: GET /api/dashboards/db/:slug response changed property `model` to `dashboard` to match the POST request nameing - Backend render URL changed from `/render/dashboard/solo` `render/dashboard-solo/` (in order to have consistent dashboard url `/dashboard/:type/:slug`) - Search HTTP API response has changed (simplified), tags list moved to seperate HTTP resource URI From 86f5152092cb28051566107d76df2d3d11883fb4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 1 Jun 2015 16:49:14 +0200 Subject: [PATCH 57/89] When role is viewer and edit URL is loaded view mode will be loaded instead, Closes #2089 --- public/app/features/dashboard/viewStateSrv.js | 7 ++++--- public/app/features/panel/panelMenu.js | 6 +++--- public/app/features/panel/panelSrv.js | 8 -------- 3 files changed, 7 insertions(+), 14 deletions(-) diff --git a/public/app/features/dashboard/viewStateSrv.js b/public/app/features/dashboard/viewStateSrv.js index ef7669e387c..e87a942a6de 100644 --- a/public/app/features/dashboard/viewStateSrv.js +++ b/public/app/features/dashboard/viewStateSrv.js @@ -130,10 +130,11 @@ function (angular, _, $) { var docHeight = $(window).height(); var editHeight = Math.floor(docHeight * 0.3); var fullscreenHeight = Math.floor(docHeight * 0.7); - this.oldTimeRange = panelScope.range; - panelScope.height = this.state.edit ? editHeight : fullscreenHeight; - panelScope.editMode = this.state.edit; + panelScope.editMode = this.state.edit && this.$scope.dashboardMeta.canEdit; + panelScope.height = panelScope.editMode ? editHeight : fullscreenHeight; + + this.oldTimeRange = panelScope.range; this.fullscreenPanel = panelScope; $(window).scrollTop(0); diff --git a/public/app/features/panel/panelMenu.js b/public/app/features/panel/panelMenu.js index 03ee78491ab..5a701084ff3 100644 --- a/public/app/features/panel/panelMenu.js +++ b/public/app/features/panel/panelMenu.js @@ -8,7 +8,7 @@ function (angular, $, _) { angular .module('grafana.directives') - .directive('panelMenu', function($compile, linkSrv, contextSrv) { + .directive('panelMenu', function($compile, linkSrv) { var linkTemplate = '' + '{{panel.title | interpolateTemplateVars:this}}' + @@ -19,7 +19,7 @@ function (angular, $, _) { function createMenuTemplate($scope) { var template = '
    '; - if ($scope.dashboardMeta.canEdit && contextSrv.isEditor) { + if ($scope.dashboardMeta.canEdit) { template += '
    '; template += '
    '; template += ''; @@ -34,7 +34,7 @@ function (angular, $, _) { _.each($scope.panelMeta.menu, function(item) { // skip edit actions if not editor - if (item.role === 'Editor' && (!contextSrv.isEditor || !$scope.dashboardMeta.canEdit)) { + if (item.role === 'Editor' && !$scope.dashboardMeta.canEdit) { return; } diff --git a/public/app/features/panel/panelSrv.js b/public/app/features/panel/panelSrv.js index d29866033ee..b863518ff72 100644 --- a/public/app/features/panel/panelSrv.js +++ b/public/app/features/panel/panelSrv.js @@ -71,14 +71,6 @@ function (angular, _, config) { }; $scope.toggleFullscreen = function(edit) { - if (edit && $scope.dashboardMeta.canEdit === false) { - $scope.appEvent('alert-warning', [ - 'Dashboard not editable', - 'Use Save As.. feature to create an editable copy of this dashboard.' - ]); - return; - } - $scope.dashboardViewState.update({ fullscreen: true, edit: edit, panelId: $scope.panel.id }); }; From ff3843bc7fe894178c21953447dae6a16985bf20 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 1 Jun 2015 17:01:04 +0200 Subject: [PATCH 58/89] Roles: New user role that replaces the old role behavior, Closes #2088 --- CHANGELOG.md | 1 + pkg/api/dashboard.go | 2 +- pkg/models/org_user.go | 9 +++++---- public/app/features/org/partials/orgUsers.html | 6 +++--- public/app/services/contextSrv.js | 2 +- 5 files changed, 11 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bc4c86878d4..9096744be61 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ **User or Organization admin** - [Issue #1899](https://github.com/grafana/grafana/issues/1899). Organization: You can now update the organization user role directly (without removing and readding the organization user). +- [Issue #2088](https://github.com/grafana/grafana/issues/2088). Roles: New user role `Read Only Editor` that replaces the old `Viewer` role behavior **Backend** - [Issue #1905](https://github.com/grafana/grafana/issues/1905). Github OAuth: You can now configure a Github team membership requirement, thx @dewski diff --git a/pkg/api/dashboard.go b/pkg/api/dashboard.go index 00ae26744d9..b010f32cdcb 100644 --- a/pkg/api/dashboard.go +++ b/pkg/api/dashboard.go @@ -55,7 +55,7 @@ func GetDashboard(c *middleware.Context) { Type: m.DashTypeDB, CanStar: c.IsSignedIn, CanSave: c.OrgRole == m.ROLE_ADMIN || c.OrgRole == m.ROLE_EDITOR, - CanEdit: c.OrgRole == m.ROLE_ADMIN || c.OrgRole == m.ROLE_EDITOR, + CanEdit: c.OrgRole == m.ROLE_ADMIN || c.OrgRole == m.ROLE_EDITOR || c.OrgRole == m.ROLE_READ_ONLY_EDITOR, }, } diff --git a/pkg/models/org_user.go b/pkg/models/org_user.go index 3e40fd24b68..afbb10386c8 100644 --- a/pkg/models/org_user.go +++ b/pkg/models/org_user.go @@ -15,13 +15,14 @@ var ( type RoleType string const ( - ROLE_VIEWER RoleType = "Viewer" - ROLE_EDITOR RoleType = "Editor" - ROLE_ADMIN RoleType = "Admin" + ROLE_VIEWER RoleType = "Viewer" + ROLE_EDITOR RoleType = "Editor" + ROLE_READ_ONLY_EDITOR RoleType = "Read Only Editor" + ROLE_ADMIN RoleType = "Admin" ) func (r RoleType) IsValid() bool { - return r == ROLE_VIEWER || r == ROLE_ADMIN || r == ROLE_EDITOR + return r == ROLE_VIEWER || r == ROLE_ADMIN || r == ROLE_EDITOR || r == ROLE_READ_ONLY_EDITOR } type OrgUser struct { diff --git a/public/app/features/org/partials/orgUsers.html b/public/app/features/org/partials/orgUsers.html index b32ff031caa..b32ffb67081 100644 --- a/public/app/features/org/partials/orgUsers.html +++ b/public/app/features/org/partials/orgUsers.html @@ -12,7 +12,7 @@
      -
    • +
    • Username or Email
    • @@ -22,7 +22,7 @@ role
    • -
    • @@ -46,7 +46,7 @@ {{user.login}} {{user.email}} - diff --git a/public/app/services/contextSrv.js b/public/app/services/contextSrv.js index aa844ee5113..b3f8a1ed164 100644 --- a/public/app/services/contextSrv.js +++ b/public/app/services/contextSrv.js @@ -60,6 +60,6 @@ function (angular, _, store, config) { store.set('grafana.sidemenu', false); } - this.isEditor = this.hasRole('Editor') || this.hasRole('Admin') || this.hasRole('Read Only Editor'); + this.isEditor = this.hasRole('Editor') || this.hasRole('Admin'); }); }); From 153ab4afaa13b5bf21f0ca88973cae60a170282b Mon Sep 17 00:00:00 2001 From: robert jakub Date: Mon, 1 Jun 2015 20:26:41 +0200 Subject: [PATCH 59/89] new role Read Only Editor - admin (small fix for #2088) --- public/app/features/admin/partials/edit_user.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/public/app/features/admin/partials/edit_user.html b/public/app/features/admin/partials/edit_user.html index 5d08292605e..a1a4cb989cd 100644 --- a/public/app/features/admin/partials/edit_user.html +++ b/public/app/features/admin/partials/edit_user.html @@ -115,7 +115,7 @@ Role
    • -
    • @@ -137,7 +137,7 @@ {{org.name}} Current - From 2446168356d6b6b4d16087ad7517ba0a15b588ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Tue, 2 Jun 2015 09:00:17 +0200 Subject: [PATCH 60/89] Sort tags in search results, Closes #2091 --- pkg/search/handlers.go | 11 +++++++-- pkg/search/handlers_test.go | 49 +++++++++++++++++++++++++++++++++++++ 2 files changed, 58 insertions(+), 2 deletions(-) create mode 100644 pkg/search/handlers_test.go diff --git a/pkg/search/handlers.go b/pkg/search/handlers.go index 874b85994ca..d0f9ae56833 100644 --- a/pkg/search/handlers.go +++ b/pkg/search/handlers.go @@ -55,12 +55,19 @@ func searchHandler(query *Query) error { hits = append(hits, jsonHits...) } - sort.Sort(hits) - + // add isStarred info if err := setIsStarredFlagOnSearchResults(query.UserId, hits); err != nil { return err } + // sort main result array + sort.Sort(hits) + + // sort tags + for _, hit := range hits { + sort.Strings(hit.Tags) + } + query.Result = hits return nil } diff --git a/pkg/search/handlers_test.go b/pkg/search/handlers_test.go new file mode 100644 index 00000000000..ebfade4dc06 --- /dev/null +++ b/pkg/search/handlers_test.go @@ -0,0 +1,49 @@ +package search + +import ( + "testing" + + "github.com/grafana/grafana/pkg/bus" + m "github.com/grafana/grafana/pkg/models" + . "github.com/smartystreets/goconvey/convey" +) + +func TestSearch(t *testing.T) { + + Convey("Given search query", t, func() { + jsonDashIndex = NewJsonDashIndex("../../public/dashboards/") + query := Query{} + + bus.AddHandler("test", func(query *FindPersistedDashboardsQuery) error { + query.Result = HitList{ + &Hit{Id: 16, Title: "CCAA", Tags: []string{"BB", "AA"}}, + &Hit{Id: 10, Title: "AABB", Tags: []string{"CC", "AA"}}, + &Hit{Id: 15, Title: "BBAA", Tags: []string{"EE", "AA", "BB"}}, + } + return nil + }) + + bus.AddHandler("test", func(query *m.GetUserStarsQuery) error { + query.Result = map[int64]bool{10: true, 12: true} + return nil + }) + + Convey("That is empty", func() { + err := searchHandler(&query) + So(err, ShouldBeNil) + + Convey("should return sorted results", func() { + So(query.Result[0].Title, ShouldEqual, "AABB") + So(query.Result[1].Title, ShouldEqual, "BBAA") + So(query.Result[2].Title, ShouldEqual, "CCAA") + }) + + Convey("should return sorted tags", func() { + So(query.Result[1].Tags[0], ShouldEqual, "AA") + So(query.Result[1].Tags[1], ShouldEqual, "BB") + So(query.Result[1].Tags[2], ShouldEqual, "EE") + }) + }) + + }) +} From dc607b8e8a8563e04cb09fa9b13f4397a486460a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Tue, 2 Jun 2015 10:24:20 +0200 Subject: [PATCH 61/89] Dashboard search now supports filtering by multiple dashboard tags, Closes #2095 --- CHANGELOG.md | 1 + pkg/api/search.go | 4 +-- pkg/search/handlers.go | 41 +++++++++++++++++++++---- pkg/search/handlers_test.go | 12 ++++++++ pkg/search/json_index.go | 7 ----- pkg/search/json_index_test.go | 6 ++-- pkg/search/models.go | 3 +- pkg/services/sqlstore/dashboard.go | 7 +---- pkg/services/sqlstore/dashboard_test.go | 12 -------- public/app/controllers/search.js | 15 ++++++--- public/app/partials/search.html | 13 +++++--- 11 files changed, 74 insertions(+), 47 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9096744be61..13b18a51fcb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ - [Issue #2088](https://github.com/grafana/grafana/issues/2088). Roles: New user role `Read Only Editor` that replaces the old `Viewer` role behavior **Backend** +- [Issue #2095](https://github.com/grafana/grafana/issues/2095). Search: Search now supports filtering by multiple dashboard tags - [Issue #1905](https://github.com/grafana/grafana/issues/1905). Github OAuth: You can now configure a Github team membership requirement, thx @dewski - [Issue #2052](https://github.com/grafana/grafana/issues/2052). Github OAuth: You can now configure a Github organization requirement, thx @indrekj - [Issue #1891](https://github.com/grafana/grafana/issues/1891). Security: New config option to disable the use of gravatar for profile images diff --git a/pkg/api/search.go b/pkg/api/search.go index 10329f445bd..4c2ba9195b6 100644 --- a/pkg/api/search.go +++ b/pkg/api/search.go @@ -8,7 +8,7 @@ import ( func Search(c *middleware.Context) { query := c.Query("query") - tag := c.Query("tag") + tags := c.QueryStrings("tag") starred := c.Query("starred") limit := c.QueryInt("limit") @@ -18,7 +18,7 @@ func Search(c *middleware.Context) { searchQuery := search.Query{ Title: query, - Tag: tag, + Tags: tags, UserId: c.UserId, Limit: limit, IsStarred: starred == "true", diff --git a/pkg/search/handlers.go b/pkg/search/handlers.go index d0f9ae56833..3b4fcb1c59f 100644 --- a/pkg/search/handlers.go +++ b/pkg/search/handlers.go @@ -33,7 +33,6 @@ func searchHandler(query *Query) error { dashQuery := FindPersistedDashboardsQuery{ Title: query.Title, - Tag: query.Tag, UserId: query.UserId, Limit: query.Limit, IsStarred: query.IsStarred, @@ -55,6 +54,22 @@ func searchHandler(query *Query) error { hits = append(hits, jsonHits...) } + // filter out results with tag filter + if len(query.Tags) > 0 { + filtered := HitList{} + for _, hit := range hits { + if hasRequiredTags(query.Tags, hit.Tags) { + filtered = append(filtered, hit) + } + } + hits = filtered + } + + // sort tags + for _, hit := range hits { + sort.Strings(hit.Tags) + } + // add isStarred info if err := setIsStarredFlagOnSearchResults(query.UserId, hits); err != nil { return err @@ -63,15 +78,29 @@ func searchHandler(query *Query) error { // sort main result array sort.Sort(hits) - // sort tags - for _, hit := range hits { - sort.Strings(hit.Tags) - } - query.Result = hits return nil } +func stringInSlice(a string, list []string) bool { + for _, b := range list { + if b == a { + return true + } + } + return false +} + +func hasRequiredTags(queryTags, hitTags []string) bool { + for _, queryTag := range queryTags { + if !stringInSlice(queryTag, hitTags) { + return false + } + } + + return true +} + func setIsStarredFlagOnSearchResults(userId int64, hits []*Hit) error { query := m.GetUserStarsQuery{UserId: userId} if err := bus.Dispatch(&query); err != nil { diff --git a/pkg/search/handlers_test.go b/pkg/search/handlers_test.go index ebfade4dc06..193ba73f94a 100644 --- a/pkg/search/handlers_test.go +++ b/pkg/search/handlers_test.go @@ -45,5 +45,17 @@ func TestSearch(t *testing.T) { }) }) + Convey("That filters by tag", func() { + query.Tags = []string{"BB", "AA"} + err := searchHandler(&query) + So(err, ShouldBeNil) + + Convey("should return correct results", func() { + So(len(query.Result), ShouldEqual, 2) + So(query.Result[0].Title, ShouldEqual, "BBAA") + So(query.Result[1].Title, ShouldEqual, "CCAA") + }) + + }) }) } diff --git a/pkg/search/json_index.go b/pkg/search/json_index.go index edf791562c9..a0fc02343e2 100644 --- a/pkg/search/json_index.go +++ b/pkg/search/json_index.go @@ -56,13 +56,6 @@ func (index *JsonDashIndex) Search(query *Query) ([]*Hit, error) { break } - // filter out results with tag filter - if query.Tag != "" { - if !strings.Contains(item.TagsCsv, query.Tag) { - continue - } - } - // add results with matchig title filter if strings.Contains(item.TitleLower, query.Title) { results = append(results, &Hit{ diff --git a/pkg/search/json_index_test.go b/pkg/search/json_index_test.go index 52741cc6806..afd584fffbd 100644 --- a/pkg/search/json_index_test.go +++ b/pkg/search/json_index_test.go @@ -17,14 +17,14 @@ func TestJsonDashIndex(t *testing.T) { }) Convey("Should be able to search index", func() { - res, err := index.Search(&Query{Title: "", Tag: "", Limit: 20}) + res, err := index.Search(&Query{Title: "", Limit: 20}) So(err, ShouldBeNil) So(len(res), ShouldEqual, 3) }) Convey("Should be able to search index by title", func() { - res, err := index.Search(&Query{Title: "home", Tag: "", Limit: 20}) + res, err := index.Search(&Query{Title: "home", Limit: 20}) So(err, ShouldBeNil) So(len(res), ShouldEqual, 1) @@ -32,7 +32,7 @@ func TestJsonDashIndex(t *testing.T) { }) Convey("Should not return when starred is filtered", func() { - res, err := index.Search(&Query{Title: "", Tag: "", IsStarred: true}) + res, err := index.Search(&Query{Title: "", IsStarred: true}) So(err, ShouldBeNil) So(len(res), ShouldEqual, 0) diff --git a/pkg/search/models.go b/pkg/search/models.go index 157d9a292e3..e65428f14b0 100644 --- a/pkg/search/models.go +++ b/pkg/search/models.go @@ -26,7 +26,7 @@ func (s HitList) Less(i, j int) bool { return s[i].Title < s[j].Title } type Query struct { Title string - Tag string + Tags []string OrgId int64 UserId int64 Limit int @@ -37,7 +37,6 @@ type Query struct { type FindPersistedDashboardsQuery struct { Title string - Tag string OrgId int64 UserId int64 Limit int diff --git a/pkg/services/sqlstore/dashboard.go b/pkg/services/sqlstore/dashboard.go index 027f2cd2fac..8756c25e13e 100644 --- a/pkg/services/sqlstore/dashboard.go +++ b/pkg/services/sqlstore/dashboard.go @@ -150,13 +150,8 @@ func SearchDashboards(query *search.FindPersistedDashboardsQuery) error { params = append(params, "%"+query.Title+"%") } - if len(query.Tag) > 0 { - sql.WriteString(" AND dashboard_tag.term=?") - params = append(params, query.Tag) - } - if query.Limit == 0 || query.Limit > 10000 { - query.Limit = 300 + query.Limit = 1000 } sql.WriteString(fmt.Sprintf(" ORDER BY dashboard.title ASC LIMIT %d", query.Limit)) diff --git a/pkg/services/sqlstore/dashboard_test.go b/pkg/services/sqlstore/dashboard_test.go index 70675f9c42b..0ddd0db0df6 100644 --- a/pkg/services/sqlstore/dashboard_test.go +++ b/pkg/services/sqlstore/dashboard_test.go @@ -99,18 +99,6 @@ func TestDashboardDataAccess(t *testing.T) { So(len(hit.Tags), ShouldEqual, 2) }) - Convey("Should be able to search for dashboards using tags", func() { - query1 := search.FindPersistedDashboardsQuery{Tag: "webapp", OrgId: 1} - query2 := search.FindPersistedDashboardsQuery{Tag: "tagdoesnotexist", OrgId: 1} - - err := SearchDashboards(&query1) - err = SearchDashboards(&query2) - So(err, ShouldBeNil) - - So(len(query1.Result), ShouldEqual, 1) - So(len(query2.Result), ShouldEqual, 0) - }) - Convey("Should not be able to save dashboard with same name", func() { cmd := m.SaveDashboardCommand{ OrgId: 1, diff --git a/public/app/controllers/search.js b/public/app/controllers/search.js index b980297df16..a762af0f887 100644 --- a/public/app/controllers/search.js +++ b/public/app/controllers/search.js @@ -14,7 +14,7 @@ function (angular, _, config) { $scope.giveSearchFocus = 0; $scope.selectedIndex = -1; $scope.results = []; - $scope.query = { query: '', tag: '', starred: false }; + $scope.query = { query: '', tag: [], starred: false }; $scope.currentSearchId = 0; if ($scope.dashboardViewState.fullscreen) { @@ -82,12 +82,11 @@ function (angular, _, config) { $scope.queryHasNoFilters = function() { var query = $scope.query; - return query.query === '' && query.starred === false && query.tag === ''; + return query.query === '' && query.starred === false && query.tag.length === 0; }; $scope.filterByTag = function(tag, evt) { - $scope.query.tag = tag; - $scope.query.tagcloud = false; + $scope.query.tag.push(tag); $scope.search(); $scope.giveSearchFocus = $scope.giveSearchFocus + 1; if (evt) { @@ -96,6 +95,14 @@ function (angular, _, config) { } }; + $scope.removeTag = function(tag, evt) { + $scope.query.tag = _.without($scope.query.tag, tag); + $scope.search(); + $scope.giveSearchFocus = $scope.giveSearchFocus + 1; + evt.stopPropagation(); + evt.preventDefault(); + }; + $scope.getTags = function() { return backendSrv.get('/api/dashboards/tags').then(function(results) { $scope.tagsMode = true; diff --git a/public/app/partials/search.html b/public/app/partials/search.html index 9c644e96d25..9eaa8d253d2 100644 --- a/public/app/partials/search.html +++ b/public/app/partials/search.html @@ -15,11 +15,14 @@ tags - - | - - {{query.tag}} - + + | + + + + {{tagName}} + +
    From 6df9012141f78f14213c6528ad131050c2bb928e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Tue, 2 Jun 2015 10:35:10 +0200 Subject: [PATCH 62/89] Updated dashboard links feature to support search by my multiple tags, #1944 --- public/app/features/dashlinks/editor.html | 3 ++- public/app/features/dashlinks/module.js | 6 +++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/public/app/features/dashlinks/editor.html b/public/app/features/dashlinks/editor.html index 576cde9c408..2d563c23c71 100644 --- a/public/app/features/dashlinks/editor.html +++ b/public/app/features/dashlinks/editor.html @@ -25,7 +25,8 @@
  • With tag
  • - + +
  • diff --git a/public/app/features/dashlinks/module.js b/public/app/features/dashlinks/module.js index e33d5406fc2..7be8ffbd87b 100644 --- a/public/app/features/dashlinks/module.js +++ b/public/app/features/dashlinks/module.js @@ -89,7 +89,7 @@ function (angular, _) { function buildLinks(linkDef) { if (linkDef.type === 'dashboards') { - if (!linkDef.tag) { + if (!linkDef.tags) { console.log('Dashboard link missing tag'); return $q.when([]); } @@ -97,7 +97,7 @@ function (angular, _) { if (linkDef.asDropdown) { return $q.when([{ title: linkDef.title, - tag: linkDef.tag, + tags: linkDef.tags, keepTime: linkDef.keepTime, includeVars: linkDef.includeVars, icon: "fa fa-bars", @@ -132,7 +132,7 @@ function (angular, _) { } $scope.searchDashboards = function(link) { - return backendSrv.search({tag: link.tag}).then(function(results) { + return backendSrv.search({tag: link.tags}).then(function(results) { return _.reduce(results, function(memo, dash) { // do not add current dashboard if (dash.id !== currentDashId) { From 50a1feb90a5535ff2ec4f6242e47e4e9209d7575 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Tue, 2 Jun 2015 11:04:06 +0200 Subject: [PATCH 63/89] Dashboard list panel: Now supports search by multiple tags, Closes #2096 --- CHANGELOG.md | 1 + pkg/api/search.go | 2 +- pkg/search/handlers.go | 13 +++++++++---- pkg/search/handlers_test.go | 2 +- pkg/search/models.go | 1 - pkg/services/sqlstore/dashboard.go | 6 +----- public/app/directives/tags.js | 9 ++++++++- public/app/panels/dashlist/editor.html | 6 +++--- public/app/panels/dashlist/module.js | 7 +++++-- 9 files changed, 29 insertions(+), 18 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 13b18a51fcb..5988579123a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ - [Issue #1888](https://github.com/grafana/grafana/issues/1144). Templating: Repeat panel or row for each selected template variable value - [Issue #1888](https://github.com/grafana/grafana/issues/1944). Dashboard: Custom Navigation links & dynamic links to related dashboards - [Issue #590](https://github.com/grafana/grafana/issues/590). Graph: Define series color using regex rule +- [Issue #2096](https://github.com/grafana/grafana/issues/2096). Dashboard list panel: Now supports search by multiple tags **User or Organization admin** - [Issue #1899](https://github.com/grafana/grafana/issues/1899). Organization: You can now update the organization user role directly (without removing and readding the organization user). diff --git a/pkg/api/search.go b/pkg/api/search.go index 4c2ba9195b6..e451ac398d6 100644 --- a/pkg/api/search.go +++ b/pkg/api/search.go @@ -13,7 +13,7 @@ func Search(c *middleware.Context) { limit := c.QueryInt("limit") if limit == 0 { - limit = 200 + limit = 1000 } searchQuery := search.Query{ diff --git a/pkg/search/handlers.go b/pkg/search/handlers.go index 3b4fcb1c59f..a3cd01e2508 100644 --- a/pkg/search/handlers.go +++ b/pkg/search/handlers.go @@ -1,6 +1,7 @@ package search import ( + "fmt" "path/filepath" "sort" @@ -34,7 +35,6 @@ func searchHandler(query *Query) error { dashQuery := FindPersistedDashboardsQuery{ Title: query.Title, UserId: query.UserId, - Limit: query.Limit, IsStarred: query.IsStarred, OrgId: query.OrgId, } @@ -65,6 +65,14 @@ func searchHandler(query *Query) error { hits = filtered } + // sort main result array + sort.Sort(hits) + + fmt.Printf("Length: %d", len(hits)) + if len(hits) > query.Limit { + hits = hits[0 : query.Limit-1] + } + // sort tags for _, hit := range hits { sort.Strings(hit.Tags) @@ -75,9 +83,6 @@ func searchHandler(query *Query) error { return err } - // sort main result array - sort.Sort(hits) - query.Result = hits return nil } diff --git a/pkg/search/handlers_test.go b/pkg/search/handlers_test.go index 193ba73f94a..dc9835caa44 100644 --- a/pkg/search/handlers_test.go +++ b/pkg/search/handlers_test.go @@ -12,7 +12,7 @@ func TestSearch(t *testing.T) { Convey("Given search query", t, func() { jsonDashIndex = NewJsonDashIndex("../../public/dashboards/") - query := Query{} + query := Query{Limit: 2000} bus.AddHandler("test", func(query *FindPersistedDashboardsQuery) error { query.Result = HitList{ diff --git a/pkg/search/models.go b/pkg/search/models.go index e65428f14b0..9b8c7627f89 100644 --- a/pkg/search/models.go +++ b/pkg/search/models.go @@ -39,7 +39,6 @@ type FindPersistedDashboardsQuery struct { Title string OrgId int64 UserId int64 - Limit int IsStarred bool Result HitList diff --git a/pkg/services/sqlstore/dashboard.go b/pkg/services/sqlstore/dashboard.go index 8756c25e13e..01eacb8436a 100644 --- a/pkg/services/sqlstore/dashboard.go +++ b/pkg/services/sqlstore/dashboard.go @@ -150,11 +150,7 @@ func SearchDashboards(query *search.FindPersistedDashboardsQuery) error { params = append(params, "%"+query.Title+"%") } - if query.Limit == 0 || query.Limit > 10000 { - query.Limit = 1000 - } - - sql.WriteString(fmt.Sprintf(" ORDER BY dashboard.title ASC LIMIT %d", query.Limit)) + sql.WriteString(fmt.Sprintf(" ORDER BY dashboard.title ASC LIMIT 1000")) var res []DashboardSearchProjection err := x.Sql(sql.String(), params...).Find(&res) diff --git a/public/app/directives/tags.js b/public/app/directives/tags.js index 4f8825a010b..f408a5e3864 100644 --- a/public/app/directives/tags.js +++ b/public/app/directives/tags.js @@ -70,7 +70,8 @@ function (angular, $) { return { restrict: 'EA', scope: { - model: '=ngModel' + model: '=ngModel', + onTagsUpdated: "&", }, template: '', replace: false, @@ -99,6 +100,9 @@ function (angular, $) { select.on('itemAdded', function(event) { if (scope.model.indexOf(event.item) === -1) { scope.model.push(event.item); + if (scope.onTagsUpdated) { + scope.onTagsUpdated(); + } } var tagElement = select.next().children("span").filter(function() { return $(this).text() === event.item; }); setColor(event.item, tagElement); @@ -108,6 +112,9 @@ function (angular, $) { var idx = scope.model.indexOf(event.item); if (idx !== -1) { scope.model.splice(idx, 1); + if (scope.onTagsUpdated) { + scope.onTagsUpdated(); + } } }); diff --git a/public/app/panels/dashlist/editor.html b/public/app/panels/dashlist/editor.html index 578d9e4b2d2..12598da9e56 100644 --- a/public/app/panels/dashlist/editor.html +++ b/public/app/panels/dashlist/editor.html @@ -27,11 +27,11 @@ ng-model="panel.query" ng-change="get_data()" ng-model-onblur>
  • - Tag + Tags
  • - + +
  • diff --git a/public/app/panels/dashlist/module.js b/public/app/panels/dashlist/module.js index da409fb3bde..9b4e1ebc045 100644 --- a/public/app/panels/dashlist/module.js +++ b/public/app/panels/dashlist/module.js @@ -32,7 +32,7 @@ function (angular, app, _, config, PanelMeta) { mode: 'starred', query: '', limit: 10, - tag: '', + tags: [] }; $scope.modes = ['starred', 'search']; @@ -43,6 +43,9 @@ function (angular, app, _, config, PanelMeta) { $scope.init = function() { panelSrv.init($scope); + if ($scope.panel.tag) { + $scope.panel.tags = [$scope.panel.tag]; + } if ($scope.isNewPanel()) { $scope.panel.title = "Starred Dashboards"; @@ -58,7 +61,7 @@ function (angular, app, _, config, PanelMeta) { params.starred = "true"; } else { params.query = $scope.panel.query; - params.tag = $scope.panel.tag; + params.tag = $scope.panel.tags; } return backendSrv.search(params).then(function(result) { From 8cfbd2f8bfd580c0e9489b45071b1c1e4ea66117 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Tue, 2 Jun 2015 11:05:48 +0200 Subject: [PATCH 64/89] Increased width of query input field in dashlist panel editor --- public/app/features/dashlinks/editor.html | 2 +- public/app/panels/dashlist/editor.html | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/public/app/features/dashlinks/editor.html b/public/app/features/dashlinks/editor.html index 2d563c23c71..69f2c067330 100644 --- a/public/app/features/dashlinks/editor.html +++ b/public/app/features/dashlinks/editor.html @@ -20,7 +20,7 @@
  • Type
  • - +
  • With tag
  • diff --git a/public/app/panels/dashlist/editor.html b/public/app/panels/dashlist/editor.html index 12598da9e56..ff2e75fd95c 100644 --- a/public/app/panels/dashlist/editor.html +++ b/public/app/panels/dashlist/editor.html @@ -23,7 +23,7 @@ Query
  • -
  • From bfe5a56a47ec6380127b2f0435ebfe470c922a29 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Tue, 2 Jun 2015 12:52:31 +0200 Subject: [PATCH 65/89] Updated dashboard links editor, just changed With tag > With tags --- public/app/features/dashlinks/editor.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/public/app/features/dashlinks/editor.html b/public/app/features/dashlinks/editor.html index 69f2c067330..886550d9b9c 100644 --- a/public/app/features/dashlinks/editor.html +++ b/public/app/features/dashlinks/editor.html @@ -20,10 +20,10 @@
  • Type
  • - +
  • -
  • With tag
  • +
  • With tags
  • From 5c8b571c3f510c4cceeb0309c6b7291344acaa22 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Tue, 2 Jun 2015 13:44:46 +0200 Subject: [PATCH 66/89] Fixed look of tag cloud in search --- public/app/partials/search.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/app/partials/search.html b/public/app/partials/search.html index 9eaa8d253d2..d4fc65afa38 100644 --- a/public/app/partials/search.html +++ b/public/app/partials/search.html @@ -33,7 +33,7 @@
    - + {{tag.term}}  ({{tag.count}}) From 7b69b789b8f8217236591a61bdbd396f6ae8fc26 Mon Sep 17 00:00:00 2001 From: Pascal Borreli Date: Tue, 2 Jun 2015 16:45:44 +0100 Subject: [PATCH 67/89] Fixed typos --- README.md | 2 +- docs/sources/datasources/opentsdb.md | 2 +- docs/sources/guides/gettingstarted.md | 4 ++-- docs/sources/installation/configuration.md | 2 +- docs/sources/project/building_from_source.md | 2 +- docs/sources/reference/annotations.md | 4 ++-- docs/sources/reference/graph.md | 2 +- docs/sources/reference/http_api.md | 4 ++-- docs/sources/reference/scripting.md | 2 +- docs/sources/reference/timerange.md | 2 +- 10 files changed, 13 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 5f0fb88b951..0a7db318338 100644 --- a/README.md +++ b/README.md @@ -87,7 +87,7 @@ go get github.com/grafana/grafana ``` cd $GOPATH/src/github.com/grafana/grafana go run build.go setup (only needed once to install godep) -godep restore (will pull down all golang lib dependecies in your current GOPATH) +godep restore (will pull down all golang lib dependencies in your current GOPATH) go build . ``` diff --git a/docs/sources/datasources/opentsdb.md b/docs/sources/datasources/opentsdb.md index 09b9f647a5f..4e85bdc9555 100644 --- a/docs/sources/datasources/opentsdb.md +++ b/docs/sources/datasources/opentsdb.md @@ -27,7 +27,7 @@ Open a graph in edit mode by click the title. ![](/img/v2/opentsdb_query_editor.png) -For details on opentsdb metric queries checkout the offical [OpenTSDB documentation](http://opentsdb.net/docs/build/html/index.html) +For details on opentsdb metric queries checkout the official [OpenTSDB documentation](http://opentsdb.net/docs/build/html/index.html) diff --git a/docs/sources/guides/gettingstarted.md b/docs/sources/guides/gettingstarted.md index b0037668f56..641dc9516f5 100644 --- a/docs/sources/guides/gettingstarted.md +++ b/docs/sources/guides/gettingstarted.md @@ -19,7 +19,7 @@ The image above shows you the top header for a dashboard. 1. Side menubar toggle: This toggles the side menu, allowing you to focus on the data presented in the dashboard. The side menu provides access to features unrelated to a Dashboard such as Users, Organizations, and Data Sources. 2. Dashboard dropdown: This dropdown shows you which Dashboard you are currently viewing, and allows you to easily switch to a new Dashboard. From here you can also create a new Dashboard, Import existing Dashboards, and manage Dashboard playlists. -3. Star Dashboard: Star (or unstar) the current Dashboar. Starred Dashboards will show up on your own Home Dashboard by default, and are a convenient way to mark Dashboards that you're interested in. +3. Star Dashboard: Star (or unstar) the current Dashboard. Starred Dashboards will show up on your own Home Dashboard by default, and are a convenient way to mark Dashboards that you're interested in. 4. Share Dashboard: Share the current dashboard by creating a link or create a static Snapshot of it. Make sure the Dashboard is saved before sharing. 5. Save dashboard: The current Dashboard will be saved with the current Dashboard name. 6. Settings: Manage Dashboard settings and features such as Templating and Annotations. @@ -28,7 +28,7 @@ The image above shows you the top header for a dashboard. Dashboards are at the core of what Grafana is all about. Dashboards are composed of individual Panels arranged on a number of Rows. By adjusting the display properties of Panels and Rows, you can customize the perfect Dashboard for your exact needs. Each panel can interact with data from any configured Grafana Data Source (currently InfluxDB, Graphite, OpenTSDB, and KairosDB). -This allows you to create a single dashboard that unifies the data across your organization. Panels use the time range specificed +This allows you to create a single dashboard that unifies the data across your organization. Panels use the time range specified in the main Time Picker in the upper right, but they can also have relative time overrides. diff --git a/docs/sources/installation/configuration.md b/docs/sources/installation/configuration.md index e27a3e80f6a..80ad5909a34 100644 --- a/docs/sources/installation/configuration.md +++ b/docs/sources/installation/configuration.md @@ -301,7 +301,7 @@ Secret. Specify these in the Grafana configuration file. For example: Restart the Grafana back-end. You should now see a Google login button on the login page. You can now login or sign up with your Google -accounts. The `allowed_domains` option is optional, and domains were seperated by space. +accounts. The `allowed_domains` option is optional, and domains were separated by space. You may allow users to sign-up via Google authentication by setting the `allow_sign_up` option to `true`. When this option is set to `true`, any diff --git a/docs/sources/project/building_from_source.md b/docs/sources/project/building_from_source.md index 8466dbd2736..8401bf543de 100644 --- a/docs/sources/project/building_from_source.md +++ b/docs/sources/project/building_from_source.md @@ -72,4 +72,4 @@ You only need to add the options you want to override. Config files are applied ## Create a pull requests -Before or after your create a pull requests, sign the [contributor license aggrement](/docs/contributing/cla.html). +Before or after your create a pull requests, sign the [contributor license agreement](/docs/contributing/cla.html). diff --git a/docs/sources/reference/annotations.md b/docs/sources/reference/annotations.md index 41d7bf411a3..51c9ac1ba32 100644 --- a/docs/sources/reference/annotations.md +++ b/docs/sources/reference/annotations.md @@ -18,9 +18,9 @@ dropdown. This will open the `Annotations` edit view. Click the `Add` tab to add Graphite supports two ways to query annotations. - A regular metric query, use the `Graphite target expression` text input for this -- Graphite events query, use the `Graphite event tags` text input, especify an tag or wildcard (leave empty should also work) +- Graphite events query, use the `Graphite event tags` text input, specify an tag or wildcard (leave empty should also work) -## Elasticsearch annoations +## Elasticsearch annotations ![](/img/v2/annotations_es.png) Grafana can query any Elasticsearch index for annotation events. The index name can be the name of an alias or an index wildcard pattern. diff --git a/docs/sources/reference/graph.md b/docs/sources/reference/graph.md index 61bebd6b37b..9de23332a99 100644 --- a/docs/sources/reference/graph.md +++ b/docs/sources/reference/graph.md @@ -62,7 +62,7 @@ The ``Left Y`` and ``Right Y`` can be customized using: - ``Unit`` - The display unit for the Y value - ``Grid Max`` - The maximum Y value. (default auto) -- ``Grid Min`` - The minium Y value. (default auto) +- ``Grid Min`` - The minimum Y value. (default auto) - ``Label`` - The Y axis label (default "") Axes can also be hidden by unchecking the appropriate box from `Show Axis`. diff --git a/docs/sources/reference/http_api.md b/docs/sources/reference/http_api.md index d888071bd35..9e24ccb39d7 100644 --- a/docs/sources/reference/http_api.md +++ b/docs/sources/reference/http_api.md @@ -84,8 +84,8 @@ Status Codes: - **401** – Unauthorized - **412** – Precondition failed -The **412** status code is used when a newer dashboard already exists (newer, its version is greater than the verison that was sent). The -same status code is also used if another dashboar exists with the same title. The response body will look like this: +The **412** status code is used when a newer dashboard already exists (newer, its version is greater than the version that was sent). The +same status code is also used if another dashboard exists with the same title. The response body will look like this: HTTP/1.1 412 Precondition Failed Content-Type: application/json; charset=UTF-8 diff --git a/docs/sources/reference/scripting.md b/docs/sources/reference/scripting.md index 45158eeadcd..d896a2c7650 100644 --- a/docs/sources/reference/scripting.md +++ b/docs/sources/reference/scripting.md @@ -12,7 +12,7 @@ With scripted dashboards you can dynamically create your dashboards using javasc under `public/dashboards/` there is a file named `scripted.js`. This file contains an example of a scripted dashboard. You can access it by using the url: `http://grafana_url/dashboard/script/scripted.js?rows=3&name=myName` -If you open scripted.js you can see how it reads url paramters from ARGS variable and then adds rows and panels. +If you open scripted.js you can see how it reads url parameters from ARGS variable and then adds rows and panels. ## Example diff --git a/docs/sources/reference/timerange.md b/docs/sources/reference/timerange.md index 47c9186e119..d2f05f5a9d6 100644 --- a/docs/sources/reference/timerange.md +++ b/docs/sources/reference/timerange.md @@ -24,7 +24,7 @@ All of this applies to all Panels in the Dashboard (except those with Panel Time It's possible to customize the options displayed for relative time and the auto-refresh options. -From Dashboard setttings, click the Timepicker tab. From here you can specify the relative and auto refresh intervals. The Timepicker tab settings are saved on a per Dashboard basis. Entries are comma seperated and accept a number followed by one of the following units: s (seconds), m (minutes), h (hours), d (days), w (weeks), M (months), y (years). +From Dashboard settings, click the Timepicker tab. From here you can specify the relative and auto refresh intervals. The Timepicker tab settings are saved on a per Dashboard basis. Entries are comma separated and accept a number followed by one of the following units: s (seconds), m (minutes), h (hours), d (days), w (weeks), M (months), y (years). ![](/img/v1/timepicker_editor.png) From f582ac88b382f72f102b43aeab33a167454590c7 Mon Sep 17 00:00:00 2001 From: Pascal Borreli Date: Tue, 2 Jun 2015 17:12:12 +0100 Subject: [PATCH 68/89] Fixed menu --- docs/mkdocs.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml index 2ff90577f07..5024fd33afe 100644 --- a/docs/mkdocs.yml +++ b/docs/mkdocs.yml @@ -61,7 +61,7 @@ pages: - ['datasources/influxdb.md', 'Data Sources', 'InfluxDB'] - ['datasources/opentsdb.md', 'Data Sources', 'OpenTSDB'] -- ['project/building_from_source.md', 'Project', 'Building from souce'] +- ['project/building_from_source.md', 'Project', 'Building from source'] - ['project/cla.md', 'Project', 'Contributor License Agreement'] - ['jsearch.md', '**HIDDEN**'] From 483ef20527a2168628342b22b39fadbe1d6f60e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Thu, 28 May 2015 11:16:16 +0200 Subject: [PATCH 69/89] Reworking variable value dropdown, take3 --- public/app/directives/metric.segment.js | 2 +- public/app/directives/variableValueSelect.js | 96 ++++++++++++------- .../partials/variableValueSelect.html | 39 ++++---- .../features/templating/partials/editor.html | 70 +++++++------- public/css/less/submenu.less | 31 +++--- public/css/less/tightform.less | 3 +- 6 files changed, 133 insertions(+), 108 deletions(-) diff --git a/public/app/directives/metric.segment.js b/public/app/directives/metric.segment.js index 05bf7e485e3..4f5677ca3ed 100644 --- a/public/app/directives/metric.segment.js +++ b/public/app/directives/metric.segment.js @@ -68,7 +68,7 @@ function (angular, app, _, $) { else { // need to have long delay because the blur // happens long before the click event on the typeahead options - cancelBlur = setTimeout($scope.switchToLink, 350); + cancelBlur = setTimeout($scope.switchToLink, 50); } }; diff --git a/public/app/directives/variableValueSelect.js b/public/app/directives/variableValueSelect.js index fa0e56acebc..42724a79df9 100644 --- a/public/app/directives/variableValueSelect.js +++ b/public/app/directives/variableValueSelect.js @@ -10,23 +10,72 @@ function (angular, app, _) { angular .module('grafana.directives') .directive('variableValueSelect', function($compile, $window, $timeout) { + + function openDropdown(inputEl, linkEl) { + inputEl.css('width', (linkEl.width() + 16) + 'px'); + + linkEl.hide(); + inputEl.show(); + inputEl.focus(); + }; + return { scope: { variable: "=", onUpdated: "&" }, + templateUrl: 'app/features/dashboard/partials/variableValueSelect.html', + link: function(scope, elem) { var bodyEl = angular.element($window.document.body); + var linkEl = elem.find('.variable-value-link'); + var inputEl = elem.find('input'); var variable = scope.variable; + var cancelBlur = null; - scope.show = function() { - if (scope.selectorOpen) { - return; + scope.openDropdown = function() { + inputEl.show(); + linkEl.hide(); + scope.dropdownVisible = true; + + inputEl.css('width', (linkEl.width() + 16) + 'px'); + + linkEl.hide(); + inputEl.show(); + inputEl.focus(); + + $timeout(function() { bodyEl.on('click', scope.bodyOnClick); }, 0, false); + }; + + scope.switchToLink = function(now) { + if (now === true || cancelBlur) { + clearTimeout(cancelBlur); + cancelBlur = null; + inputEl.hide(); + linkEl.show(); + scope.dropdownVisible = false; + scope.$digest(); + + scope.updateLinkText(); + scope.onUpdated(); + } + else { + // need to have long delay because the blur + // happens long before the click event on the typeahead options + cancelBlur = setTimeout(scope.switchToLink, 50); } - scope.selectorOpen = true; - scope.giveFocus = 1; + bodyEl.off('click', scope.bodyOnClick); + }; + + scope.bodyOnClick = function(e) { + if (elem.has(e.target).length === 0) { + scope.switchToLink(); + } + }; + + scope.show = function() { scope.oldCurrentText = variable.current.text; scope.highlightIndex = -1; @@ -45,9 +94,7 @@ function (angular, app, _) { scope.search = {query: '', options: scope.options}; - $timeout(function() { - bodyEl.on('click', scope.bodyOnClick); - }, 0, false); + scope.openDropdown(); }; scope.queryChanged = function() { @@ -79,7 +126,7 @@ function (angular, app, _) { scope.optionSelected = function(option, event) { option.selected = !option.selected; - var hideAfter = true; + var hideAfter = false; var setAllExceptCurrentTo = function(newValue) { _.each(scope.options, function(other) { if (option !== other) { other.selected = newValue; } @@ -91,13 +138,10 @@ function (angular, app, _) { } else if (!variable.multi) { setAllExceptCurrentTo(false); - } else { - if (event.ctrlKey || event.metaKey || event.shiftKey) { - hideAfter = false; - } - else { - setAllExceptCurrentTo(false); - } + hideAfter = true; + } else if (event.ctrlKey || event.metaKey || event.shiftKey) { + hideAfter = true; + setAllExceptCurrentTo(false); } var selected = _.filter(scope.options, {selected: true}); @@ -124,23 +168,8 @@ function (angular, app, _) { variable.current.value = selected[0].value; } - scope.updateLinkText(); - scope.onUpdated(); - if (hideAfter) { - scope.hide(); - } - }; - - scope.hide = function() { - scope.selectorOpen = false; - bodyEl.off('click', scope.bodyOnClick); - }; - - scope.bodyOnClick = function(e) { - var dropdown = elem.find('.variable-value-dropdown'); - if (dropdown.has(e.target).length === 0) { - scope.$apply(scope.hide); + scope.switchToLink(); } }; @@ -152,6 +181,9 @@ function (angular, app, _) { scope.$watchGroup(['variable.hideLabel', 'variable.name', 'variable.label', 'variable.current.text'], function() { scope.updateLinkText(); }); + + linkEl.click(scope.openDropdown); + //inputEl.blur(scope.switchToLink); }, }; }); diff --git a/public/app/features/dashboard/partials/variableValueSelect.html b/public/app/features/dashboard/partials/variableValueSelect.html index 481f2734876..afa4bdbe649 100644 --- a/public/app/features/dashboard/partials/variableValueSelect.html +++ b/public/app/features/dashboard/partials/variableValueSelect.html @@ -1,35 +1,28 @@ - + {{labelText}}:
    - + {{linkText}} + -
    -
    - - - + diff --git a/public/app/features/templating/partials/editor.html b/public/app/features/templating/partials/editor.html index d1232c72dcf..99b50d45866 100644 --- a/public/app/features/templating/partials/editor.html +++ b/public/app/features/templating/partials/editor.html @@ -226,41 +226,41 @@
    - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +
    +
    +
    value groups/tags
    +
    +
      +
    • + tags query +
    • +
    • + +
    • +
    +
    +
    +
    +
      +
    • + tags values query +
    • +
    • + +
    • +
    +
    +
    +
    +
      +
    • + +
    • +
    +
    +
    +
    +
    diff --git a/public/css/less/submenu.less b/public/css/less/submenu.less index 4fefa50d5e7..a92cb163537 100644 --- a/public/css/less/submenu.less +++ b/public/css/less/submenu.less @@ -20,7 +20,7 @@ } .submenu-item { - padding: 8px 7px; +// padding: 8px 7px; margin-right: 20px; display: inline-block; border-radius: 3px; @@ -43,10 +43,10 @@ .variable-value-dropdown { position: absolute; - top: 27px; + top: 47px; min-width: 150px; max-height: 400px; - background: @grafanaPanelBackground; + background: @dropdownBackground; box-shadow: 0px 0px 55px 0px black; border: 1px solid @grafanaTargetFuncBackground; z-index: 1000; @@ -74,22 +74,23 @@ .variable-option, .variable-options-column-header { display: block; - padding: 0 27px 0 8px; + padding: 2px 27px 0 8px; position: relative; + white-space: nowrap; + min-width: 115px; - .variable-option-icon { display: none } + .variable-option-icon { + display: inline-block; + width: 24px; + height: 18px; + position: relative; + top: 4px; + background: url(@checkboxImageUrl) left top no-repeat; + } &.selected { - .variable-option-icon:before { - content: "\f00c"; - } - .variable-option-icon { - display: block; - padding-left: 4px; - line-height: 26px; - position: absolute; - right: 0; - top: 0; + .variable-option-icon{ + background: url(@checkboxImageUrl) 0px -18px no-repeat; } } } diff --git a/public/css/less/tightform.less b/public/css/less/tightform.less index 5bd9cda8f43..71457a62141 100644 --- a/public/css/less/tightform.less +++ b/public/css/less/tightform.less @@ -23,7 +23,7 @@ .tight-form-container-no-item-borders { border: 1px solid @grafanaTargetBorder; - .tight-form, .tight-form-item, [type=text].tight-form-input { + .tight-form, .tight-form-item, [type=text].tight-form-input, [type=text].tight-form-clear-input { border: none; } } @@ -132,7 +132,6 @@ input[type=text].tight-form-clear-input { border: none; margin: 0px; background: transparent; - float: left; color: @grafanaTargetColor; border-radius: 0; border-right: 1px solid @grafanaTargetSegmentBorder; From a433e0e79c10a74264b0d456f5596ef7c5092180 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Thu, 28 May 2015 11:39:08 +0200 Subject: [PATCH 70/89] More work on variable dropdown --- public/app/directives/variableValueSelect.js | 27 +++++++++++++++----- public/app/features/templating/editorCtrl.js | 1 + 2 files changed, 21 insertions(+), 7 deletions(-) diff --git a/public/app/directives/variableValueSelect.js b/public/app/directives/variableValueSelect.js index 42724a79df9..43c1014160e 100644 --- a/public/app/directives/variableValueSelect.js +++ b/public/app/directives/variableValueSelect.js @@ -115,7 +115,10 @@ function (angular, app, _) { scope.moveHighlight(-1); } if (evt.keyCode === 13) { - scope.optionSelected(scope.search.options[scope.highlightIndex], {}); + scope.optionSelected(scope.search.options[scope.highlightIndex], {}, true, false); + } + if (evt.keyCode === 32) { + scope.optionSelected(scope.search.options[scope.highlightIndex], {}, false, false); } }; @@ -123,24 +126,34 @@ function (angular, app, _) { scope.highlightIndex = (scope.highlightIndex + direction) % scope.search.options.length; }; - scope.optionSelected = function(option, event) { + scope.optionSelected = function(option, event, commitChange, excludeOthers) { + if (!option) { return; } + option.selected = !option.selected; - var hideAfter = false; + commitChange = commitChange || false; + excludeOthers = excludeOthers || false; + var setAllExceptCurrentTo = function(newValue) { _.each(scope.options, function(other) { if (option !== other) { other.selected = newValue; } }); }; - if (option.text === 'All') { + // commit action (enter key), should not deselect it + if (commitChange) { + option.selected = true; + } + + if (option.text === 'All' || excludeOthers) { setAllExceptCurrentTo(false); + commitChange = true; } else if (!variable.multi) { setAllExceptCurrentTo(false); - hideAfter = true; + commitChange = true; } else if (event.ctrlKey || event.metaKey || event.shiftKey) { - hideAfter = true; + commitChange = true; setAllExceptCurrentTo(false); } @@ -168,7 +181,7 @@ function (angular, app, _) { variable.current.value = selected[0].value; } - if (hideAfter) { + if (commitChange) { scope.switchToLink(); } }; diff --git a/public/app/features/templating/editorCtrl.js b/public/app/features/templating/editorCtrl.js index f48452e4569..aeb27b3c832 100644 --- a/public/app/features/templating/editorCtrl.js +++ b/public/app/features/templating/editorCtrl.js @@ -82,6 +82,7 @@ function (angular, _) { }; $scope.update = function() { + $scope.current.tags = []; if ($scope.isValid()) { $scope.runQuery().then(function() { $scope.reset(); From 9a741051037398f3937fe646694fd8d4709d24c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Thu, 28 May 2015 12:52:47 +0200 Subject: [PATCH 71/89] Trying out dark headers and footers for template dropdown --- public/app/directives/variableValueSelect.js | 5 ++- .../partials/variableValueSelect.html | 32 ++++++++++++------- public/css/less/submenu.less | 29 ++++++++++------- 3 files changed, 42 insertions(+), 24 deletions(-) diff --git a/public/app/directives/variableValueSelect.js b/public/app/directives/variableValueSelect.js index 43c1014160e..e74564369c2 100644 --- a/public/app/directives/variableValueSelect.js +++ b/public/app/directives/variableValueSelect.js @@ -93,6 +93,7 @@ function (angular, app, _) { }); scope.search = {query: '', options: scope.options}; + scope.selectedValuesCount = currentValues.length; scope.openDropdown(); }; @@ -176,8 +177,10 @@ function (angular, app, _) { value: _.pluck(selected, 'value'), }; + scope.selectedValuesCount = variable.current.value.length; + // only single value - if (variable.current.value.length === 1) { + if (scope.selectedValuesCount === 1) { variable.current.value = selected[0].value; } diff --git a/public/app/features/dashboard/partials/variableValueSelect.html b/public/app/features/dashboard/partials/variableValueSelect.html index afa4bdbe649..3bbc5060c0e 100644 --- a/public/app/features/dashboard/partials/variableValueSelect.html +++ b/public/app/features/dashboard/partials/variableValueSelect.html @@ -10,19 +10,27 @@
    -
    - - - - {{option.text}} - +
    +
    +
    + Selected ({{selectedValuesCount}}) +
    + + + {{option.text}} + +
    +
    -
    - - - - {{tag}}    - +
    diff --git a/public/css/less/submenu.less b/public/css/less/submenu.less index a92cb163537..de12ca4a108 100644 --- a/public/css/less/submenu.less +++ b/public/css/less/submenu.less @@ -46,7 +46,10 @@ top: 47px; min-width: 150px; max-height: 400px; + min-height: 150px; background: @dropdownBackground; + overflow-y: auto; + overflow-x: hidden; box-shadow: 0px 0px 55px 0px black; border: 1px solid @grafanaTargetFuncBackground; z-index: 1000; @@ -62,7 +65,6 @@ .variable-options-column { max-height: 350px; - overflow: auto; display: table-cell; line-height: 26px; &:nth-child(2) { @@ -95,22 +97,27 @@ } } +.variable-options-column-header { + background-color: @bodyBackground; + text-align: center; + padding-top: 5px; + padding-bottom: 5px; + margin-bottom: 5px; +} + +.variable-options-footer { + background-color: @bodyBackground; + text-align: center; + padding-top: 5px; + padding-bottom: 5px; +} + .variable-option { &:hover, &.highlighted { background-color: @blueDark; } } -.variable-search-wrapper { - input { - width: 100%; - padding: 7px 8px; - height: 100%; - box-sizing: border-box; - margin-bottom: 6px; - } -} - .dash-nav-link { color: @textColor; } From 0bd50c06d77015fa9826e6f082daac9b488b9f3b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Thu, 28 May 2015 13:02:01 +0200 Subject: [PATCH 72/89] Template varaible dropdown work --- .../features/dashboard/partials/variableValueSelect.html | 4 ---- public/css/less/submenu.less | 8 -------- 2 files changed, 12 deletions(-) diff --git a/public/app/features/dashboard/partials/variableValueSelect.html b/public/app/features/dashboard/partials/variableValueSelect.html index 3bbc5060c0e..de22c9cc54e 100644 --- a/public/app/features/dashboard/partials/variableValueSelect.html +++ b/public/app/features/dashboard/partials/variableValueSelect.html @@ -28,9 +28,5 @@
    -
    diff --git a/public/css/less/submenu.less b/public/css/less/submenu.less index de12ca4a108..cbdff413a0b 100644 --- a/public/css/less/submenu.less +++ b/public/css/less/submenu.less @@ -98,20 +98,12 @@ } .variable-options-column-header { - background-color: @bodyBackground; text-align: center; padding-top: 5px; padding-bottom: 5px; margin-bottom: 5px; } -.variable-options-footer { - background-color: @bodyBackground; - text-align: center; - padding-top: 5px; - padding-bottom: 5px; -} - .variable-option { &:hover, &.highlighted { background-color: @blueDark; From 7d25d6f1915e56477504416584ccdf8f48ad5d5c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Thu, 28 May 2015 18:38:53 +0200 Subject: [PATCH 73/89] progress on tag selection in variable dropdown --- public/app/directives/variableValueSelect.js | 47 ++++++++++++++++++- .../partials/variableValueSelect.html | 8 +++- public/app/features/templating/editorCtrl.js | 1 - public/css/less/overrides.less | 2 +- public/css/less/submenu.less | 3 ++ 5 files changed, 55 insertions(+), 6 deletions(-) diff --git a/public/app/directives/variableValueSelect.js b/public/app/directives/variableValueSelect.js index e74564369c2..c22154f68a6 100644 --- a/public/app/directives/variableValueSelect.js +++ b/public/app/directives/variableValueSelect.js @@ -95,6 +95,12 @@ function (angular, app, _) { scope.search = {query: '', options: scope.options}; scope.selectedValuesCount = currentValues.length; + if (!scope.tags) { + scope.tags = _.map(variable.tags, function(value) { + return { text: value, selected: false }; + }); + } + scope.openDropdown(); }; @@ -158,11 +164,15 @@ function (angular, app, _) { setAllExceptCurrentTo(false); } + scope.selectionsChanged(option, commitChange); + }; + + scope.selectionsChanged = function(defaultItem, commitChange) { var selected = _.filter(scope.options, {selected: true}); if (selected.length === 0) { - option.selected = true; - selected = [option]; + defaultItem.selected = true; + selected = [defaultItem]; } if (selected.length > 1 && selected.length !== scope.options.length) { @@ -177,6 +187,18 @@ function (angular, app, _) { value: _.pluck(selected, 'value'), }; + var valuesNotInTag = _.filter(selected, function(test) { + for (var i = 0; i < scope.selectedTags.length; i++) { + var tag = scope.selectedTags[i]; + if (_.indexOf(tag.values, test.value) !== -1) { + return false; + } + } + return true; + }); + + variable.current.text = _.pluck(valuesNotInTag, 'text').join(', '); + scope.selectedValuesCount = variable.current.value.length; // only single value @@ -189,6 +211,27 @@ function (angular, app, _) { } }; + scope.selectTag = function(tag) { + tag.selected = !tag.selected; + if (!tag.values) { + if (tag.text === 'backend') { + tag.values = ['backend_01', 'backend_02', 'backend_03', 'backend_04']; + } else { + tag.values = ['web_server_01', 'web_server_02', 'web_server_03', 'web_server_04']; + } + console.log('querying for tag values'); + } + + _.each(scope.options, function(option) { + if (_.indexOf(tag.values, option.value) !== -1) { + option.selected = tag.selected; + } + }); + + scope.selectedTags = _.filter(scope.tags, {selected: true}); + scope.selectionsChanged(scope.options[0], false); + }; + scope.updateLinkText = function() { scope.labelText = variable.label || '$' + variable.name; scope.linkText = variable.current.text; diff --git a/public/app/features/dashboard/partials/variableValueSelect.html b/public/app/features/dashboard/partials/variableValueSelect.html index de22c9cc54e..bb4f0ac03e6 100644 --- a/public/app/features/dashboard/partials/variableValueSelect.html +++ b/public/app/features/dashboard/partials/variableValueSelect.html @@ -5,6 +5,10 @@
    diff --git a/public/app/features/templating/editorCtrl.js b/public/app/features/templating/editorCtrl.js index aeb27b3c832..f48452e4569 100644 --- a/public/app/features/templating/editorCtrl.js +++ b/public/app/features/templating/editorCtrl.js @@ -82,7 +82,6 @@ function (angular, _) { }; $scope.update = function() { - $scope.current.tags = []; if ($scope.isValid()) { $scope.runQuery().then(function() { $scope.reset(); diff --git a/public/css/less/overrides.less b/public/css/less/overrides.less index b03cb7f7e6b..6ac6a385b60 100644 --- a/public/css/less/overrides.less +++ b/public/css/less/overrides.less @@ -542,7 +542,7 @@ div.flot-text { background-color: @purple; color: darken(@white, 5%); white-space: nowrap; - border-radius: 2px; + border-radius: 3px; text-shadow: none; font-size: 13px; padding: 2px 6px; diff --git a/public/css/less/submenu.less b/public/css/less/submenu.less index cbdff413a0b..e2d6b0639dd 100644 --- a/public/css/less/submenu.less +++ b/public/css/less/submenu.less @@ -39,6 +39,9 @@ .variable-value-link { font-size: 16px; padding-right: 10px; + .label-tag { + margin: 0 5px; + } } .variable-value-dropdown { From 6ed17fe62fe692a9a9961849b4c19653fc8efc3a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Fri, 29 May 2015 08:21:44 +0200 Subject: [PATCH 74/89] Removed selection state for single select variables --- public/app/directives/variableValueSelect.js | 15 ++------ .../partials/variableValueSelect.html | 9 ++--- public/css/less/submenu.less | 34 +++++++++++-------- 3 files changed, 27 insertions(+), 31 deletions(-) diff --git a/public/app/directives/variableValueSelect.js b/public/app/directives/variableValueSelect.js index c22154f68a6..f51c706bfe5 100644 --- a/public/app/directives/variableValueSelect.js +++ b/public/app/directives/variableValueSelect.js @@ -11,20 +11,8 @@ function (angular, app, _) { .module('grafana.directives') .directive('variableValueSelect', function($compile, $window, $timeout) { - function openDropdown(inputEl, linkEl) { - inputEl.css('width', (linkEl.width() + 16) + 'px'); - - linkEl.hide(); - inputEl.show(); - inputEl.focus(); - }; - return { - scope: { - variable: "=", - onUpdated: "&" - }, - + scope: { variable: "=", onUpdated: "&" }, templateUrl: 'app/features/dashboard/partials/variableValueSelect.html', link: function(scope, elem) { @@ -94,6 +82,7 @@ function (angular, app, _) { scope.search = {query: '', options: scope.options}; scope.selectedValuesCount = currentValues.length; + scope.selectedTags = scope.selectedTag || []; if (!scope.tags) { scope.tags = _.map(variable.tags, function(value) { diff --git a/public/app/features/dashboard/partials/variableValueSelect.html b/public/app/features/dashboard/partials/variableValueSelect.html index bb4f0ac03e6..303e1756033 100644 --- a/public/app/features/dashboard/partials/variableValueSelect.html +++ b/public/app/features/dashboard/partials/variableValueSelect.html @@ -2,7 +2,7 @@ {{labelText}}: -
    +
  • diff --git a/public/test/specs/selectDropdownCtrl-specs.js b/public/test/specs/selectDropdownCtrl-specs.js index bd9f0919d20..c919b3b8bee 100644 --- a/public/test/specs/selectDropdownCtrl-specs.js +++ b/public/test/specs/selectDropdownCtrl-specs.js @@ -8,11 +8,17 @@ function () { describe("SelectDropdownCtrl", function() { var scope; var ctrl; + var tagValuesMap = {}; + var rootScope; beforeEach(module('grafana.controllers')); - beforeEach(inject(function($controller, $rootScope) { + beforeEach(inject(function($controller, $rootScope, $q) { + rootScope = $rootScope; scope = $rootScope.$new(); ctrl = $controller('SelectDropdownCtrl', {$scope: scope}); + ctrl.getValuesForTag = function(obj) { + return $q.when(tagValuesMap[obj.tagKey]); + }; })); describe("Given simple variable", function() { @@ -24,9 +30,71 @@ function () { it("Should init labelText and linkText", function() { expect(ctrl.linkText).to.be("hej"); }); - }); - }); + describe("Given variable with tags and dropdown is opened", function() { + beforeEach(function() { + ctrl.variable = { + current: {text: 'hej', value: 'hej'}, + options: [ + {text: 'server-1', value: 'server-1'}, + {text: 'server-2', value: 'server-2'}, + {text: 'server-3', value: 'server-3'}, + ], + tags: ["key1", "key2", "key3"] + }; + tagValuesMap.key1 = ['server-1', 'server-3']; + tagValuesMap.key2 = ['server-2', 'server-3']; + tagValuesMap.key3 = ['server-1', 'server-2', 'server-3']; + ctrl.init(); + ctrl.show(); + }); + it("should init tags model", function() { + expect(ctrl.tags.length).to.be(3); + expect(ctrl.tags[0].text).to.be("key1"); + }); + + it("should init options model", function() { + expect(ctrl.options.length).to.be(3); + }); + + describe('When tag is selected', function() { + beforeEach(function() { + ctrl.selectTag(ctrl.tags[0]); + rootScope.$digest(); + }); + + it("should select tag", function() { + expect(ctrl.selectedTags.length).to.be(1); + }); + + it("should select values", function() { + expect(ctrl.options[0].selected).to.be(true); + expect(ctrl.options[2].selected).to.be(true); + }); + + describe('and then unselected', function() { + beforeEach(function() { + ctrl.selectTag(ctrl.tags[0]); + rootScope.$digest(); + }); + + it("should deselect tag", function() { + expect(ctrl.selectedTags.length).to.be(0); + }); + }); + + describe('and then value is unselected', function() { + beforeEach(function() { + ctrl.optionSelected(ctrl.options[0]); + }); + + it("should deselect tag", function() { + expect(ctrl.selectedTags.length).to.be(0); + }); + }); + }); + }); + }); }); From 650d3d504687bfb4934890a96017f0bef8eddf05 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Fri, 29 May 2015 17:51:07 +0200 Subject: [PATCH 80/89] Fixed tag selection issues --- public/app/directives/variableValueSelect.js | 6 ++---- public/app/features/templating/templateValuesSrv.js | 2 +- public/test/specs/selectDropdownCtrl-specs.js | 12 ++++++++++++ 3 files changed, 15 insertions(+), 5 deletions(-) diff --git a/public/app/directives/variableValueSelect.js b/public/app/directives/variableValueSelect.js index f77510afc0d..41f8d5b1322 100644 --- a/public/app/directives/variableValueSelect.js +++ b/public/app/directives/variableValueSelect.js @@ -31,7 +31,7 @@ function (angular, app, _) { vm.search = {query: '', options: vm.options}; vm.selectedValuesCount = currentValues.length; - vm.selectedTags = vm.selectedTag || []; + vm.selectedTags = vm.selectedTags || []; if (!vm.tags) { vm.tags = _.map(vm.variable.tags, function(value) { @@ -76,7 +76,6 @@ function (angular, app, _) { } }); - vm.selectedTags = _.filter(vm.tags, {selected: true}); vm.selectionsChanged(false); }); }; @@ -168,10 +167,9 @@ function (angular, app, _) { return true; }); - vm.variable.current = {}; vm.variable.current.value = _.pluck(selected, 'value'); vm.variable.current.text = _.pluck(valuesNotInTag, 'text').join(', '); - vm.selectedValuesCount = vm.variable.current.value.length; + vm.selectedValuesCount = selected.length; // only single value if (vm.selectedValuesCount === 1) { diff --git a/public/app/features/templating/templateValuesSrv.js b/public/app/features/templating/templateValuesSrv.js index 7330be1b5cd..2fd17f4aa65 100644 --- a/public/app/features/templating/templateValuesSrv.js +++ b/public/app/features/templating/templateValuesSrv.js @@ -78,7 +78,7 @@ function (angular, _, kbn) { }; this.setVariableValue = function(variable, option) { - variable.current = option; + variable.current = angular.copy(option); templateSrv.updateTemplateData(); return this.updateOptionsInChildVariables(variable); }; diff --git a/public/test/specs/selectDropdownCtrl-specs.js b/public/test/specs/selectDropdownCtrl-specs.js index c919b3b8bee..968320b8e4f 100644 --- a/public/test/specs/selectDropdownCtrl-specs.js +++ b/public/test/specs/selectDropdownCtrl-specs.js @@ -74,6 +74,18 @@ function () { expect(ctrl.options[2].selected).to.be(true); }); + describe('and then dropdown is opened and closed without changes', function() { + beforeEach(function() { + ctrl.show(); + ctrl.commitChanges(); + rootScope.$digest(); + }); + + it("should still have selected tag", function() { + expect(ctrl.selectedTags.length).to.be(1); + }); + }); + describe('and then unselected', function() { beforeEach(function() { ctrl.selectTag(ctrl.tags[0]); From f48d0fcb13b193e5006b2bb54b7a6877eb7fa27c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Sat, 30 May 2015 09:34:11 +0200 Subject: [PATCH 81/89] More work on tags in variable dropdown, now the actual tag values query is hooked up and works, #2080 --- public/app/directives/variableValueSelect.js | 5 ++++- public/app/features/dashboard/submenuCtrl.js | 8 +++----- .../features/templating/partials/editor.html | 20 +++++++++---------- .../features/templating/templateValuesSrv.js | 11 ++++++++++ public/app/partials/submenu.html | 2 +- 5 files changed, 29 insertions(+), 17 deletions(-) diff --git a/public/app/directives/variableValueSelect.js b/public/app/directives/variableValueSelect.js index 41f8d5b1322..bdc914c144d 100644 --- a/public/app/directives/variableValueSelect.js +++ b/public/app/directives/variableValueSelect.js @@ -13,7 +13,7 @@ function (angular, app, _) { var vm = this; vm.show = function() { - vm.oldCurrentText = vm.variable.current.text; + vm.oldLinkText = vm.variable.current.text; vm.highlightIndex = -1; var currentValues = vm.variable.current.value; @@ -44,6 +44,9 @@ function (angular, app, _) { vm.updateLinkText = function() { vm.linkText = vm.variable.current.text; + if (vm.oldLinkText && vm.oldLinkText !== vm.linkText) { + vm.onUpdated(); + } }; vm.clearSelections = function() { diff --git a/public/app/features/dashboard/submenuCtrl.js b/public/app/features/dashboard/submenuCtrl.js index 6f423ebbe59..b8e609c061e 100644 --- a/public/app/features/dashboard/submenuCtrl.js +++ b/public/app/features/dashboard/submenuCtrl.js @@ -18,9 +18,7 @@ function (angular, _) { $scope.panel = $scope.pulldown; $scope.row = $scope.pulldown; $scope.annotations = $scope.dashboard.templating.list; - $scope.variables = _.map($scope.dashboard.templating.list, function(variable) { - return variable; - }); + $scope.variables = $scope.dashboard.templating.list; }; $scope.disableAnnotation = function (annotation) { @@ -28,8 +26,8 @@ function (angular, _) { $rootScope.$broadcast('refresh'); }; - $scope.getValuesForTag = function() { - return $q.when(['backend_01', 'backend_02']); + $scope.getValuesForTag = function(variable, tagKey) { + return templateValuesSrv.getValuesForTag(variable, tagKey); }; $scope.variableUpdated = function(variable) { diff --git a/public/app/features/templating/partials/editor.html b/public/app/features/templating/partials/editor.html index 99b50d45866..382790e0f21 100644 --- a/public/app/features/templating/partials/editor.html +++ b/public/app/features/templating/partials/editor.html @@ -228,25 +228,25 @@
    -
    value groups/tags
    -
    +
    Value groups/tags
    +
      -
    • - tags query +
    • + Tags query
    • - +
    -
    +
      -
    • - tags values query +
    • + Tag values query
    • - +
    @@ -254,7 +254,7 @@
    • - +
    diff --git a/public/app/features/templating/templateValuesSrv.js b/public/app/features/templating/templateValuesSrv.js index 2fd17f4aa65..8b61b40688d 100644 --- a/public/app/features/templating/templateValuesSrv.js +++ b/public/app/features/templating/templateValuesSrv.js @@ -154,6 +154,17 @@ function (angular, _, kbn) { }); }; + this.getValuesForTag = function(variable, tagKey) { + return datasourceSrv.get(variable.datasource).then(function(datasource) { + var query = variable.tagValuesQuery.replace('$tag', tagKey); + return datasource.metricFindQuery(query).then(function (results) { + return _.map(results, function(value) { + return value.text; + }); + }); + }); + }; + this.metricNamesToVariableValues = function(variable, metricNames) { var regex, options, i, matches; options = {}; // use object hash to remove duplicates diff --git a/public/app/partials/submenu.html b/public/app/partials/submenu.html index 43a5581e0b5..5f5d29e87ba 100644 --- a/public/app/partials/submenu.html +++ b/public/app/partials/submenu.html @@ -6,7 +6,7 @@ {{variable.label || variable.name}}: - + From b5a846154a3ca821c4f781591987c2cc295e7c6c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 1 Jun 2015 10:43:25 +0200 Subject: [PATCH 82/89] Trying to make progres on persisting selection state, restoring selection state for new multi variable dropdown, proving to be really complex --- public/app/directives/variableValueSelect.js | 88 +++++++++---------- .../partials/variableValueSelect.html | 4 +- .../features/templating/templateValuesSrv.js | 4 + public/test/specs/selectDropdownCtrl-specs.js | 33 ++++++- 4 files changed, 79 insertions(+), 50 deletions(-) diff --git a/public/app/directives/variableValueSelect.js b/public/app/directives/variableValueSelect.js index bdc914c144d..0c3ff0e3507 100644 --- a/public/app/directives/variableValueSelect.js +++ b/public/app/directives/variableValueSelect.js @@ -13,24 +13,20 @@ function (angular, app, _) { var vm = this; vm.show = function() { - vm.oldLinkText = vm.variable.current.text; + vm.oldVariableText = vm.variable.current.text; vm.highlightIndex = -1; var currentValues = vm.variable.current.value; - if (_.isString(currentValues)) { currentValues = [currentValues]; } vm.options = _.map(vm.variable.options, function(option) { - if (_.indexOf(currentValues, option.value) >= 0) { - option.selected = true; - } + if (_.indexOf(currentValues, option.value) >= 0) { option.selected = true; } return option; }); - vm.search = {query: '', options: vm.options}; - vm.selectedValuesCount = currentValues.length; + vm.selectedValues = _.filter(vm.options, {selected: true}); vm.selectedTags = vm.selectedTags || []; if (!vm.tags) { @@ -39,14 +35,26 @@ function (angular, app, _) { }); } + vm.search = {query: '', options: vm.options}; vm.dropdownVisible = true; }; vm.updateLinkText = function() { + // var currentValues = vm.variable.current.text; + // + // if (vm.variable.current.tags) { + // selectedOptions = _.filter(selectedOptions, function(test) { + // for (var i = 0; i < vm.variable.current.tags; i++) { + // var tag = vm.selectedTags[i]; + // if (_.indexOf(tag.values, test.text) !== -1) { + // return false; + // } + // } + // return true; + // }); + // } + // vm.linkText = vm.variable.current.text; - if (vm.oldLinkText && vm.oldLinkText !== vm.linkText) { - vm.onUpdated(); - } }; vm.clearSelections = function() { @@ -62,17 +70,13 @@ function (angular, app, _) { var tagValuesPromise; if (!tag.values) { tagValuesPromise = vm.getValuesForTag({tagKey: tag.text}); - // if (tag.text === 'backend') { - // tag.values = ['backend_01', 'backend_02', 'backend_03', 'backend_04']; - // } else { - // tag.values = ['web_server_01', 'web_server_02', 'web_server_03', 'web_server_04']; - // } } else { tagValuesPromise = $q.when(tag.values); } tagValuesPromise.then(function(values) { tag.values = values; + tag.valuesText = values.join(', '); _.each(vm.options, function(option) { if (_.indexOf(tag.values, option.value) !== -1) { option.selected = tag.selected; @@ -105,7 +109,7 @@ function (angular, app, _) { vm.highlightIndex = (vm.highlightIndex + direction) % vm.search.options.length; }; - vm.optionSelected = function(option, event, commitChange, excludeOthers) { + vm.selectValue = function(option, event, commitChange, excludeOthers) { if (!option) { return; } option.selected = !option.selected; @@ -140,43 +144,34 @@ function (angular, app, _) { }; vm.selectionsChanged = function(commitChange) { - var selected = _.filter(vm.options, {selected: true}); + vm.selectedValues = _.filter(vm.options, {selected: true}); - if (selected.length > 1 && selected.length !== vm.options.length) { - if (selected[0].text === 'All') { - selected[0].selected = false; - selected = selected.slice(1, selected.length); + if (vm.selectedValues.length > 1 && vm.selectedValues.length !== vm.options.length) { + if (vm.selectedValues[0].text === 'All') { + vm.selectedValues[0].selected = false; + vm.selectedValues = vm.selectedValues.slice(1, vm.selectedValues.length); } } // validate selected tags - _.each(vm.selectedTags, function(tag) { - _.each(tag.values, function(value) { - if (!_.findWhere(selected, {value: value})) { - tag.selected = false; - } - }); + _.each(vm.tags, function(tag) { + if (tag.selected) { + _.each(tag.values, function(value) { + if (!_.findWhere(vm.selectedValues, {value: value})) { + tag.selected = false; + } + }); + } }); vm.selectedTags = _.filter(vm.tags, {selected: true}); - - var valuesNotInTag = _.filter(selected, function(test) { - for (var i = 0; i < vm.selectedTags.length; i++) { - var tag = vm.selectedTags[i]; - if (_.indexOf(tag.values, test.value) !== -1) { - return false; - } - } - return true; - }); - - vm.variable.current.value = _.pluck(selected, 'value'); - vm.variable.current.text = _.pluck(valuesNotInTag, 'text').join(', '); - vm.selectedValuesCount = selected.length; + vm.variable.current.value = _.pluck(vm.selectedValues, 'value'); + vm.variable.current.text = _.pluck(vm.selectedValues, 'text').join(' + '); + vm.variable.current.tags = vm.selectedTags; // only single value - if (vm.selectedValuesCount === 1) { - vm.variable.current.value = selected[0].value; + if (vm.selectedValues.length === 1) { + vm.variable.current.value = vm.selectedValues[0].value; } if (commitChange) { @@ -186,14 +181,17 @@ function (angular, app, _) { vm.commitChanges = function() { // make sure one option is selected - var selected = _.filter(vm.options, {selected: true}); - if (selected.length === 0) { + if (vm.selectedValues.length === 0) { vm.options[0].selected = true; vm.selectionsChanged(false); } vm.dropdownVisible = false; vm.updateLinkText(); + + if (vm.variable.current.text !== vm.oldVariableText) { + vm.onUpdated(); + } }; vm.queryChanged = function() { diff --git a/public/app/features/dashboard/partials/variableValueSelect.html b/public/app/features/dashboard/partials/variableValueSelect.html index 169e868337b..011522d6afe 100644 --- a/public/app/features/dashboard/partials/variableValueSelect.html +++ b/public/app/features/dashboard/partials/variableValueSelect.html @@ -15,9 +15,9 @@
    - Selected ({{vm.selectedValuesCount}}) + Selected ({{vm.selectedValues.length}}) - + {{option.text}} diff --git a/public/app/features/templating/templateValuesSrv.js b/public/app/features/templating/templateValuesSrv.js index 8b61b40688d..1cfa9bfc4ad 100644 --- a/public/app/features/templating/templateValuesSrv.js +++ b/public/app/features/templating/templateValuesSrv.js @@ -130,6 +130,10 @@ function (angular, _, kbn) { // if parameter has current value // if it exists in options array keep value if (variable.current) { + // if current value is an array do not do anything + if (_.isArray(variable.current.value)) { + return $q.when([]); + } var currentOption = _.findWhere(variable.options, { text: variable.current.text }); if (currentOption) { return self.setVariableValue(variable, currentOption); diff --git a/public/test/specs/selectDropdownCtrl-specs.js b/public/test/specs/selectDropdownCtrl-specs.js index 968320b8e4f..603acc9a5fb 100644 --- a/public/test/specs/selectDropdownCtrl-specs.js +++ b/public/test/specs/selectDropdownCtrl-specs.js @@ -19,6 +19,7 @@ function () { ctrl.getValuesForTag = function(obj) { return $q.when(tagValuesMap[obj.tagKey]); }; + ctrl.onUpdated = sinon.spy(); })); describe("Given simple variable", function() { @@ -35,13 +36,14 @@ function () { describe("Given variable with tags and dropdown is opened", function() { beforeEach(function() { ctrl.variable = { - current: {text: 'hej', value: 'hej'}, + current: {text: 'server-1', value: 'server-1'}, options: [ {text: 'server-1', value: 'server-1'}, {text: 'server-2', value: 'server-2'}, {text: 'server-3', value: 'server-3'}, ], - tags: ["key1", "key2", "key3"] + tags: ["key1", "key2", "key3"], + multi: true }; tagValuesMap.key1 = ['server-1', 'server-3']; tagValuesMap.key2 = ['server-2', 'server-3']; @@ -59,10 +61,30 @@ function () { expect(ctrl.options.length).to.be(3); }); + it("should init selected values array", function() { + expect(ctrl.selectedValues.length).to.be(1); + }); + + it("should set linkText", function() { + expect(ctrl.linkText).to.be('server-1'); + }); + + describe('after adititional value is selected', function() { + beforeEach(function() { + ctrl.selectValue(ctrl.options[2], {}); + ctrl.commitChanges(); + }); + + it('should update link text', function() { + expect(ctrl.linkText).to.be('server-1 + server-3'); + }); + }); + describe('When tag is selected', function() { beforeEach(function() { ctrl.selectTag(ctrl.tags[0]); rootScope.$digest(); + ctrl.commitChanges(); }); it("should select tag", function() { @@ -72,6 +94,11 @@ function () { it("should select values", function() { expect(ctrl.options[0].selected).to.be(true); expect(ctrl.options[2].selected).to.be(true); + expect(ctrl.linkText).to.be('server-1 + server-2'); + }); + + it("link text should not include tag values", function() { + expect(ctrl.linkText).to.not.contain('server-1'); }); describe('and then dropdown is opened and closed without changes', function() { @@ -99,7 +126,7 @@ function () { describe('and then value is unselected', function() { beforeEach(function() { - ctrl.optionSelected(ctrl.options[0]); + ctrl.selectValue(ctrl.options[0], {}); }); it("should deselect tag", function() { From b0451dc1b3483b875f5973f8e817574907139e1a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 1 Jun 2015 19:32:50 +0200 Subject: [PATCH 83/89] More refinements of tag selection and state restoration after dashboard load --- public/app/directives/variableValueSelect.js | 48 ++++++++++++------- public/test/specs/selectDropdownCtrl-specs.js | 3 +- 2 files changed, 33 insertions(+), 18 deletions(-) diff --git a/public/app/directives/variableValueSelect.js b/public/app/directives/variableValueSelect.js index 0c3ff0e3507..c89485130aa 100644 --- a/public/app/directives/variableValueSelect.js +++ b/public/app/directives/variableValueSelect.js @@ -27,7 +27,6 @@ function (angular, app, _) { }); vm.selectedValues = _.filter(vm.options, {selected: true}); - vm.selectedTags = vm.selectedTags || []; if (!vm.tags) { vm.tags = _.map(vm.variable.tags, function(value) { @@ -40,21 +39,37 @@ function (angular, app, _) { }; vm.updateLinkText = function() { - // var currentValues = vm.variable.current.text; - // - // if (vm.variable.current.tags) { - // selectedOptions = _.filter(selectedOptions, function(test) { - // for (var i = 0; i < vm.variable.current.tags; i++) { - // var tag = vm.selectedTags[i]; - // if (_.indexOf(tag.values, test.text) !== -1) { - // return false; - // } - // } - // return true; - // }); - // } - // - vm.linkText = vm.variable.current.text; + var current = vm.variable.current; + var currentValues = current.value; + + if (_.isArray(currentValues) && current.tags.length) { + // filer out values that are in selected tags + currentValues = _.filter(currentValues, function(test) { + for (var i = 0; i < current.tags.length; i++) { + if (_.indexOf(current.tags[i].values, test) !== -1) { + return false; + } + } + return true; + }); + // convert values to text + var currentTexts = _.map(currentValues, function(value) { + for (var i = 0; i < vm.variable.options.length; i++) { + var option = vm.variable.options[i]; + if (option.value === value) { + return option.text; + } + } + return value; + }); + // join texts + vm.linkText = currentTexts.join(' + '); + if (vm.linkText.length > 0) { + vm.linkText += ' + '; + } + } else { + vm.linkText = vm.variable.current.text; + } }; vm.clearSelections = function() { @@ -202,6 +217,7 @@ function (angular, app, _) { }; vm.init = function() { + vm.selectedTags = vm.variable.current.tags || []; vm.updateLinkText(); }; diff --git a/public/test/specs/selectDropdownCtrl-specs.js b/public/test/specs/selectDropdownCtrl-specs.js index 603acc9a5fb..3aae3429a7c 100644 --- a/public/test/specs/selectDropdownCtrl-specs.js +++ b/public/test/specs/selectDropdownCtrl-specs.js @@ -94,11 +94,10 @@ function () { it("should select values", function() { expect(ctrl.options[0].selected).to.be(true); expect(ctrl.options[2].selected).to.be(true); - expect(ctrl.linkText).to.be('server-1 + server-2'); }); it("link text should not include tag values", function() { - expect(ctrl.linkText).to.not.contain('server-1'); + expect(ctrl.linkText).to.be(''); }); describe('and then dropdown is opened and closed without changes', function() { From 8934c83742a8a6cc0dc5126cc3bbcacfddb21efb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Tue, 2 Jun 2015 14:39:21 +0200 Subject: [PATCH 84/89] Small fixes for template variable groups (tags) --- public/app/directives/variableValueSelect.js | 2 ++ public/app/features/dashboard/partials/variableValueSelect.html | 2 +- public/app/features/templating/templateValuesSrv.js | 1 + 3 files changed, 4 insertions(+), 1 deletion(-) diff --git a/public/app/directives/variableValueSelect.js b/public/app/directives/variableValueSelect.js index c89485130aa..2ddb0dd932c 100644 --- a/public/app/directives/variableValueSelect.js +++ b/public/app/directives/variableValueSelect.js @@ -26,6 +26,8 @@ function (angular, app, _) { return option; }); + _.sortBy(vm.options, 'text'); + vm.selectedValues = _.filter(vm.options, {selected: true}); if (!vm.tags) { diff --git a/public/app/features/dashboard/partials/variableValueSelect.html b/public/app/features/dashboard/partials/variableValueSelect.html index 011522d6afe..25d220ab3a5 100644 --- a/public/app/features/dashboard/partials/variableValueSelect.html +++ b/public/app/features/dashboard/partials/variableValueSelect.html @@ -2,8 +2,8 @@ {{vm.linkText}} - {{tag.text}}     + {{tag.text}} diff --git a/public/app/features/templating/templateValuesSrv.js b/public/app/features/templating/templateValuesSrv.js index 1cfa9bfc4ad..267a465cd89 100644 --- a/public/app/features/templating/templateValuesSrv.js +++ b/public/app/features/templating/templateValuesSrv.js @@ -153,6 +153,7 @@ function (angular, _, kbn) { }); }); } else { + delete variable.tags; return queryPromise; } }); From cb63344394c7048bb44153b16695588a7bf36d40 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Tue, 2 Jun 2015 19:51:12 +0200 Subject: [PATCH 85/89] Added hover tooltip for tags --- public/app/directives/variableValueSelect.js | 2 +- .../features/dashboard/partials/variableValueSelect.html | 8 +++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/public/app/directives/variableValueSelect.js b/public/app/directives/variableValueSelect.js index 2ddb0dd932c..94984977c7a 100644 --- a/public/app/directives/variableValueSelect.js +++ b/public/app/directives/variableValueSelect.js @@ -93,7 +93,7 @@ function (angular, app, _) { tagValuesPromise.then(function(values) { tag.values = values; - tag.valuesText = values.join(', '); + tag.valuesText = values.join(' + '); _.each(vm.options, function(option) { if (_.indexOf(tag.values, option.value) !== -1) { option.selected = tag.selected; diff --git a/public/app/features/dashboard/partials/variableValueSelect.html b/public/app/features/dashboard/partials/variableValueSelect.html index 25d220ab3a5..2c291448573 100644 --- a/public/app/features/dashboard/partials/variableValueSelect.html +++ b/public/app/features/dashboard/partials/variableValueSelect.html @@ -1,9 +1,11 @@