diff --git a/CHANGELOG.md b/CHANGELOG.md index 3af11520726..5b2faa9a319 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,12 +1,22 @@ # 2.2 (unreleased) -**New Features && Enhancements** +** New Feature: Mix data sources ** +A built in data source is now available named `-- Mixed --`, When picked in the metrics tab, +it allows you to add queries of differnet data source types & instances to the same graph/panel! +[Issue #436](https://github.com/grafana/grafana/issues/436) + +** Other new Features && Enhancements** - [Issue #2457](https://github.com/grafana/grafana/issues/2457). Admin: admin page for all grafana organizations (list / edit view) - [Issue #1186](https://github.com/grafana/grafana/issues/1186). Time Picker: New option `today`, will set time range from midnight to now **Fixes** - [Issue #2490](https://github.com/grafana/grafana/issues/2490). Graphite: Dashboard import was broken in 2.1 and 2.1.1, working now +**Breaking Changes** +- Notice to makers/users of custom data sources, there is a minor breaking change in 2.2 that +require and update to custom data sources for them to work in 2.2. [Read this doc](https://github.com/grafana/grafana/tree/master/docs/sources/datasources/plugin_api.md) for more on the +data source api change. + # 2.1.x (currently unreleased patch branch) **Fixes** diff --git a/docs/sources/datasources/plugin_api.md b/docs/sources/datasources/plugin_api.md new file mode 100644 index 00000000000..cdcaca29460 --- /dev/null +++ b/docs/sources/datasources/plugin_api.md @@ -0,0 +1,40 @@ +---- +page_title: Data source Plugin API +page_description: Data Source Plugin Description +page_keywords: grafana, data source, plugin, api, docs +--- + +# Data source plugin API + +All data sources in Grafana are implemented as plugins. + +## Breaking change in 2.2 + +In Grafana 2.2 a breaking change was introduced for how data source query editors +are structured, defined and loaded. This was in order to support mixing multiple data sources +in the same panel. + +In Grafana 2.2, the query editor is no longer defined using the partials section in +`plugin.json`, but defined via an angular directive named using convention naming +scheme like `metricQueryEditor`. For example + +Graphite defines a directive like this: + +```javascript +module.directive('metricQueryEditorGraphite', function() { + return {controller: 'GraphiteQueryCtrl', templateUrl: 'app/plugins/datasource/graphite/partials/query.editor.html'}; +}); +``` + +Even though the data source type name is with lowercase `g`, the directive uses capital `G` in `Graphite` because +that is how angular directives needs to be named in order to match an element with name ``. +You also specify the query controller here instead of in the query.editor.html partial like before. + +### query.editor.html + +This partial needs to be updated, remove the `np-repeat` this is done in the outer partial now,m the query.editor.html +should only render a single query. Take a look at the Graphite or InfluxDB partials for `query.editor.html` for reference. +You should also add a `tight-form-item` with `{{target.refId}}`, all queries needs to be assigned a letter (`refId`). +These query reference letters are going to be utilized in a later feature. + + diff --git a/pkg/api/datasources.go b/pkg/api/datasources.go index e0253df3cdb..717211d438d 100644 --- a/pkg/api/datasources.go +++ b/pkg/api/datasources.go @@ -112,5 +112,13 @@ func UpdateDataSource(c *middleware.Context, cmd m.UpdateDataSourceCommand) { } func GetDataSourcePlugins(c *middleware.Context) { - c.JSON(200, plugins.DataSources) + dsList := make(map[string]interface{}) + + for key, value := range plugins.DataSources { + if value.(map[string]interface{})["builtIn"] == nil { + dsList[key] = value + } + } + + c.JSON(200, dsList) } diff --git a/pkg/api/frontendsettings.go b/pkg/api/frontendsettings.go index 7851f1d8f0d..4442f004b82 100644 --- a/pkg/api/frontendsettings.go +++ b/pkg/api/frontendsettings.go @@ -86,11 +86,17 @@ func getFrontendSettingsMap(c *middleware.Context) (map[string]interface{}, erro // add grafana backend data source grafanaDatasourceMeta, _ := plugins.DataSources["grafana"] - datasources["grafana"] = map[string]interface{}{ + datasources["-- Grafana --"] = map[string]interface{}{ "type": "grafana", "meta": grafanaDatasourceMeta, } + // add mixed backend data source + datasources["-- Mixed --"] = map[string]interface{}{ + "type": "mixed", + "meta": plugins.DataSources["mixed"], + } + if defaultDatasource == "" { defaultDatasource = "grafana" } diff --git a/public/app/directives/giveFocus.js b/public/app/directives/giveFocus.js index ef395d27fbd..6493676e0f9 100644 --- a/public/app/directives/giveFocus.js +++ b/public/app/directives/giveFocus.js @@ -16,8 +16,11 @@ function (angular) { } setTimeout(function() { element.focus(); - var pos = element.val().length * 2; - element[0].setSelectionRange(pos, pos); + var domEl = element[0]; + if (domEl.setSelectionRange) { + var pos = element.val().length * 2; + domEl.setSelectionRange(pos, pos); + } }, 200); },true); }; diff --git a/public/app/directives/metric.segment.js b/public/app/directives/metric.segment.js index c585be21291..3b5f473b6dc 100644 --- a/public/app/directives/metric.segment.js +++ b/public/app/directives/metric.segment.js @@ -15,7 +15,7 @@ function (angular, app, _, $) { ' spellcheck="false" style="display:none">'; var buttonTemplate = ''; + 'tabindex="1" give-focus="segment.focus" ng-bind-html="segment.html">'; return { scope: { diff --git a/public/app/features/annotations/partials/editor.html b/public/app/features/annotations/partials/editor.html index 799b1f69fe4..2ff4040d098 100644 --- a/public/app/features/annotations/partials/editor.html +++ b/public/app/features/annotations/partials/editor.html @@ -72,8 +72,7 @@ -
-
+
diff --git a/public/app/features/panel/panelDirective.js b/public/app/features/panel/panelDirective.js index 7330bb627de..bb60d209aad 100644 --- a/public/app/features/panel/panelDirective.js +++ b/public/app/features/panel/panelDirective.js @@ -6,35 +6,108 @@ define([ function (angular, $, config) { 'use strict'; - angular - .module('grafana.directives') - .directive('panelLoader', function($compile, $parse) { - return { - restrict: 'E', - link: function(scope, elem, attr) { - var getter = $parse(attr.type), panelType = getter(scope); - var panelPath = config.panels[panelType].path; + var module = angular.module('grafana.directives'); - scope.require([panelPath + "/module"], function () { - var panelEl = angular.element(document.createElement('grafana-panel-' + panelType)); + module.directive('panelLoader', function($compile, $parse) { + return { + restrict: 'E', + link: function(scope, elem, attr) { + var getter = $parse(attr.type), panelType = getter(scope); + var panelPath = config.panels[panelType].path; + + scope.require([panelPath + "/module"], function () { + var panelEl = angular.element(document.createElement('grafana-panel-' + panelType)); + elem.append(panelEl); + $compile(panelEl)(scope); + }); + } + }; + }); + + module.directive('grafanaPanel', function() { + return { + restrict: 'E', + templateUrl: 'app/features/panel/partials/panel.html', + transclude: true, + link: function(scope, elem) { + var panelContainer = elem.find('.panel-container'); + + scope.$watchGroup(['fullscreen', 'height', 'panel.height', 'row.height'], function() { + panelContainer.css({ minHeight: scope.height || scope.panel.height || scope.row.height, display: 'block' }); + elem.toggleClass('panel-fullscreen', scope.fullscreen ? true : false); + }); + } + }; + }); + + module.service('dynamicDirectiveSrv', function($compile, $parse, datasourceSrv) { + var self = this; + + this.addDirective = function(options, type, editorScope) { + var panelEl = angular.element(document.createElement(options.name + '-' + type)); + options.parentElem.append(panelEl); + $compile(panelEl)(editorScope); + }; + + this.define = function(options) { + var editorScope; + options.scope.$watch(options.datasourceProperty, function(newVal) { + if (editorScope) { + editorScope.$destroy(); + options.parentElem.empty(); + } + + editorScope = options.scope.$new(); + datasourceSrv.get(newVal).then(function(ds) { + self.addDirective(options, ds.meta.type, editorScope); + }); + }); + }; + }); + + module.directive('queryEditorLoader', function($compile, $parse, datasourceSrv) { + return { + restrict: 'E', + link: function(scope, elem) { + var editorScope; + + scope.$watch("panel.datasource", function() { + var datasource = scope.target.datasource || scope.panel.datasource; + + datasourceSrv.get(datasource).then(function(ds) { + if (editorScope) { + editorScope.$destroy(); + elem.empty(); + } + + editorScope = scope.$new(); + editorScope.datasource = ds; + + if (!scope.target.refId) { + scope.target.refId = 'A'; + } + + var panelEl = angular.element(document.createElement('metric-query-editor-' + ds.meta.type)); elem.append(panelEl); - $compile(panelEl)(scope); + $compile(panelEl)(editorScope); }); - } - }; - }).directive('grafanaPanel', function() { - return { - restrict: 'E', - templateUrl: 'app/features/panel/partials/panel.html', - transclude: true, - link: function(scope, elem) { - var panelContainer = elem.find('.panel-container'); + }); + } + }; + }); + + module.directive('datasourceEditorView', function(dynamicDirectiveSrv) { + return { + restrict: 'E', + link: function(scope, elem, attrs) { + dynamicDirectiveSrv.define({ + datasourceProperty: attrs.datasource, + name: attrs.name, + scope: scope, + parentElem: elem, + }); + } + }; + }); - scope.$watchGroup(['fullscreen', 'height', 'panel.height', 'row.height'], function() { - panelContainer.css({ minHeight: scope.height || scope.panel.height || scope.row.height, display: 'block' }); - elem.toggleClass('panel-fullscreen', scope.fullscreen ? true : false); - }); - } - }; - }); }); diff --git a/public/app/features/panel/panelSrv.js b/public/app/features/panel/panelSrv.js index 037125b4aad..bf8c983c018 100644 --- a/public/app/features/panel/panelSrv.js +++ b/public/app/features/panel/panelSrv.js @@ -43,8 +43,21 @@ function (angular, _, config) { }); }; - $scope.addDataQuery = function() { - $scope.panel.targets.push({target: ''}); + $scope.addDataQuery = function(datasource) { + var letters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'; + var target = {}; + + if (datasource) { + target.datasource = datasource.name; + } + + target.refId = _.find(letters, function(refId) { + return _.every($scope.panel.targets, function(other) { + return other.refId !== refId; + }); + }); + + $scope.panel.targets.push(target); }; $scope.removeDataQuery = function (query) { @@ -53,7 +66,23 @@ function (angular, _, config) { }; $scope.setDatasource = function(datasource) { - $scope.panel.datasource = datasource; + // switching to mixed + if (datasource.meta.mixed) { + _.each($scope.panel.targets, function(target) { + target.datasource = $scope.panel.datasource; + if (target.datasource === null) { + target.datasource = config.defaultDatasource; + } + }); + } + // switching from mixed + else if ($scope.datasource && $scope.datasource.meta.mixed) { + _.each($scope.panel.targets, function(target) { + delete target.datasource; + }); + } + + $scope.panel.datasource = datasource.value; $scope.datasource = null; $scope.get_data(); }; diff --git a/public/app/features/templating/editorCtrl.js b/public/app/features/templating/editorCtrl.js index f48452e4569..65b06f3ceb3 100644 --- a/public/app/features/templating/editorCtrl.js +++ b/public/app/features/templating/editorCtrl.js @@ -23,7 +23,10 @@ function (angular, _) { $scope.init = function() { $scope.editor = { index: 0 }; - $scope.datasources = datasourceSrv.getMetricSources(); + $scope.datasources = _.filter(datasourceSrv.getMetricSources(), function(ds) { + return !ds.meta.builtIn; + }); + $scope.variables = templateSrv.variables; $scope.reset(); diff --git a/public/app/partials/metrics.html b/public/app/partials/metrics.html index db304d1c232..215bc2dd945 100644 --- a/public/app/partials/metrics.html +++ b/public/app/partials/metrics.html @@ -1,24 +1,48 @@ -
+
+
+ + +
-
- - - + + +
+ + +
+ +
+ +
diff --git a/public/app/plugins/datasource/elasticsearch/datasource.js b/public/app/plugins/datasource/elasticsearch/datasource.js index 9568b66ad74..bd23f0cc2b2 100644 --- a/public/app/plugins/datasource/elasticsearch/datasource.js +++ b/public/app/plugins/datasource/elasticsearch/datasource.js @@ -3,7 +3,8 @@ define([ 'lodash', 'config', 'kbn', - 'moment' + 'moment', + './directives' ], function (angular, _, config, kbn, moment) { 'use strict'; diff --git a/public/app/plugins/datasource/elasticsearch/directives.js b/public/app/plugins/datasource/elasticsearch/directives.js new file mode 100644 index 00000000000..8ab75f8e4ad --- /dev/null +++ b/public/app/plugins/datasource/elasticsearch/directives.js @@ -0,0 +1,13 @@ +define([ + 'angular', +], +function (angular) { + 'use strict'; + + var module = angular.module('grafana.directives'); + + module.directive('annotationsQueryEditorElasticsearch', function() { + return {templateUrl: 'app/plugins/datasource/elasticsearch/partials/annotations.editor.html'}; + }); + +}); diff --git a/public/app/plugins/datasource/grafana/datasource.js b/public/app/plugins/datasource/grafana/datasource.js index b0beea99e53..16c16211d13 100644 --- a/public/app/plugins/datasource/grafana/datasource.js +++ b/public/app/plugins/datasource/grafana/datasource.js @@ -2,6 +2,7 @@ define([ 'angular', 'lodash', 'kbn', + './directives', ], function (angular, _, kbn) { 'use strict'; @@ -13,16 +14,6 @@ function (angular, _, kbn) { function GrafanaDatasource() { } - GrafanaDatasource.prototype.getDashboard = function(slug, isTemp) { - var url = '/dashboards/' + slug; - - if (isTemp) { - url = '/temp/' + slug; - } - - return backendSrv.get('/api/dashboards/db/' + slug); - }; - GrafanaDatasource.prototype.query = function(options) { // get from & to in seconds var from = kbn.parseDate(options.range.from).getTime(); @@ -35,36 +26,6 @@ function (angular, _, kbn) { return $q.when([]); }; - GrafanaDatasource.prototype.starDashboard = function(dashId) { - return backendSrv.post('/api/user/stars/dashboard/' + dashId); - }; - - GrafanaDatasource.prototype.unstarDashboard = function(dashId) { - return backendSrv.delete('/api/user/stars/dashboard/' + dashId); - }; - - GrafanaDatasource.prototype.saveDashboard = function(dashboard) { - return backendSrv.post('/api/dashboards/db/', { dashboard: dashboard }) - .then(function(data) { - return { title: dashboard.title, url: '/dashboard/db/' + data.slug }; - }, function(err) { - err.isHandled = true; - err.data = err.data || {}; - throw err.data.message || "Unknown error"; - }); - }; - - GrafanaDatasource.prototype.deleteDashboard = function(id) { - return backendSrv.delete('/api/dashboards/db/' + id); - }; - - GrafanaDatasource.prototype.searchDashboards = function(query) { - return backendSrv.get('/api/search/', query) - .then(function(data) { - return data; - }); - }; - return GrafanaDatasource; }); diff --git a/public/app/plugins/datasource/grafana/directives.js b/public/app/plugins/datasource/grafana/directives.js new file mode 100644 index 00000000000..9c29340e430 --- /dev/null +++ b/public/app/plugins/datasource/grafana/directives.js @@ -0,0 +1,13 @@ +define([ + 'angular', +], +function (angular) { + 'use strict'; + + var module = angular.module('grafana.directives'); + + module.directive('metricQueryEditorGrafana', function() { + return {templateUrl: 'app/plugins/datasource/grafana/partials/query.editor.html'}; + }); + +}); diff --git a/public/app/plugins/datasource/grafana/partials/query.editor.html b/public/app/plugins/datasource/grafana/partials/query.editor.html index 2ab843827e2..15297d5c3f0 100644 --- a/public/app/plugins/datasource/grafana/partials/query.editor.html +++ b/public/app/plugins/datasource/grafana/partials/query.editor.html @@ -1,16 +1,56 @@ +
+ -
-
-
-
Test graph
- -

- This is just a test data source that generates random walk series. If this is your only data source - open the left side menu and navigate to the data sources admin screen and add your data sources (you need to be - logged in to do this). You can change data source using the button to the left of the Add query button. -

-
-
- +
    +
  • + {{target.refId}} +
  • +
  • + + + +
  • +
  • + Test metric (fake data source) +
  • +
diff --git a/public/app/plugins/datasource/grafana/plugin.json b/public/app/plugins/datasource/grafana/plugin.json index 9358cb29af1..b32045b9ef8 100644 --- a/public/app/plugins/datasource/grafana/plugin.json +++ b/public/app/plugins/datasource/grafana/plugin.json @@ -1,15 +1,11 @@ { "pluginType": "datasource", - "name": "Grafana (for testing)", + "name": "Grafana", + "builtIn": true, "type": "grafana", "serviceName": "GrafanaDatasource", "module": "plugins/datasource/grafana/datasource", - - "partials": { - "query": "app/plugins/datasource/grafana/partials/query.editor.html" - }, - "metrics": true } diff --git a/public/app/plugins/datasource/graphite/datasource.js b/public/app/plugins/datasource/graphite/datasource.js index e9f2a6efe5d..400ec7abc77 100644 --- a/public/app/plugins/datasource/graphite/datasource.js +++ b/public/app/plugins/datasource/graphite/datasource.js @@ -5,6 +5,7 @@ define([ 'config', 'kbn', 'moment', + './directives', './queryCtrl', './funcEditor', './addGraphiteFunc', @@ -228,21 +229,13 @@ function (angular, _, $, config, kbn, moment) { return backendSrv.datasourceRequest(options); }; - GraphiteDatasource.prototype._seriesRefLetters = [ - '#A', '#B', '#C', '#D', - '#E', '#F', '#G', '#H', - '#I', '#J', '#K', '#L', - '#M', '#N', '#O', '#P', - '#Q', '#R', '#S', '#T', - '#U', '#V', '#W', '#X', - '#Y', '#Z' - ]; + GraphiteDatasource.prototype._seriesRefLetters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'; GraphiteDatasource.prototype.buildGraphiteParams = function(options, scopedVars) { var graphite_options = ['from', 'until', 'rawData', 'format', 'maxDataPoints', 'cacheTimeout']; var clean_options = [], targets = {}; var target, targetValue, i; - var regex = /(\#[A-Z])/g; + var regex = /\#([A-Z])/g; var intervalFormatFixRegex = /'(\d+)m'/gi; if (options.format !== 'png') { @@ -259,13 +252,17 @@ function (angular, _, $, config, kbn, moment) { continue; } + if (!target.refId) { + target.refId = this._seriesRefLetters[i]; + } + targetValue = templateSrv.replace(target.target, scopedVars); targetValue = targetValue.replace(intervalFormatFixRegex, fixIntervalFormat); - targets[this._seriesRefLetters[i]] = targetValue; + targets[target.refId] = targetValue; } - function nestedSeriesRegexReplacer(match) { - return targets[match]; + function nestedSeriesRegexReplacer(match, g1) { + return targets[g1]; } for (i = 0; i < options.targets.length; i++) { @@ -274,9 +271,9 @@ function (angular, _, $, config, kbn, moment) { continue; } - targetValue = targets[this._seriesRefLetters[i]]; + targetValue = targets[target.refId]; targetValue = targetValue.replace(regex, nestedSeriesRegexReplacer); - targets[this._seriesRefLetters[i]] = targetValue; + targets[target.refId] = targetValue; if (!target.hide) { clean_options.push("target=" + encodeURIComponent(targetValue)); diff --git a/public/app/plugins/datasource/graphite/directives.js b/public/app/plugins/datasource/graphite/directives.js new file mode 100644 index 00000000000..91e52bb9546 --- /dev/null +++ b/public/app/plugins/datasource/graphite/directives.js @@ -0,0 +1,21 @@ +define([ + 'angular', +], +function (angular) { + 'use strict'; + + var module = angular.module('grafana.directives'); + + module.directive('metricQueryEditorGraphite', function() { + return {controller: 'GraphiteQueryCtrl', templateUrl: 'app/plugins/datasource/graphite/partials/query.editor.html'}; + }); + + module.directive('metricQueryOptionsGraphite', function() { + return {templateUrl: 'app/plugins/datasource/graphite/partials/query.options.html'}; + }); + + module.directive('annotationsQueryEditorGraphite', function() { + return {templateUrl: 'app/plugins/datasource/graphite/partials/annotations.editor.html'}; + }); + +}); diff --git a/public/app/plugins/datasource/graphite/partials/query.editor.html b/public/app/plugins/datasource/graphite/partials/query.editor.html index add85c26773..3608c2573cb 100755 --- a/public/app/plugins/datasource/graphite/partials/query.editor.html +++ b/public/app/plugins/datasource/graphite/partials/query.editor.html @@ -1,221 +1,72 @@ -
- -
- +
+ +
  • + + + +
  • + -
      -
    • - {{targetLetters[$index]}} -
    • -
    • - - - -
    • -
    +
      +
    • + {{target.refId}} +
    • +
    • + + + +
    • +
    - + - -
    -
    -
    - -
    -
    -
      -
    • - +
    - -
    - -
    -
    - -
    -
    Shorter legend names
    -
      -
    • alias() function to specify a custom series name
    • -
    • aliasByNode(2) to alias by a specific part of your metric path
    • -
    • aliasByNode(2, -1) you can add multiple segment paths, and use negative index
    • -
    • groupByNode(2, 'sum') is useful if you have 2 wildcards in your metric path and want to sumSeries and group by
    • -
    -
    - -
    -
    Series as parameter
    -
      -
    • Some graphite functions allow you to have many series arguments
    • -
    • Use #[A-Z] to use a graphite query as parameter to a function
    • -
    • - Examples: -
        -
      • asPercent(#A, #B)
      • -
      • prod.srv-01.counters.count - asPercent(#A) : percentage of count in comparison with A query
      • -
      • prod.srv-01.counters.count - sumSeries(#A) : sum count and series A
      • -
      • divideSeries(#A, #B)
      • -
      -
    • -
    • If a query is added only to be used as a parameter, hide it from the graph with the eye icon
    • -
    -
    - -
    -
    Stacking
    -
      -
    • You find the stacking option under Display Styles tab
    • -
    • When stacking is enabled make sure null point mode is set to 'null as zero'
    • -
    -
    - -
    -
    Templating
    -
      -
    • You can use a template variable in place of metric names
    • -
    • You can use a template variable in place of function parameters
    • -
    • You enable the templating feature in Dashboard settings / Feature toggles
    • -
    -
    - -
    -
    Max data points
    -
      -
    • Every graphite request is issued with a maxDataPoints parameter
    • -
    • Graphite uses this parameter to consolidate the real number of values down to this number
    • -
    • If there are more real values, then by default they will be consolidated using averages
    • -
    • This could hide real peaks and max values in your series
    • -
    • You can change how point consolidation is made using the consolidateBy graphite function
    • -
    • Point consolidation will effect series legend values (min,max,total,current)
    • -
    • If you override maxDataPoint and set a high value performance can be severely effected
    • -
    -
    - -
    diff --git a/public/app/plugins/datasource/graphite/partials/query.options.html b/public/app/plugins/datasource/graphite/partials/query.options.html new file mode 100644 index 00000000000..f42a9b59ea7 --- /dev/null +++ b/public/app/plugins/datasource/graphite/partials/query.options.html @@ -0,0 +1,132 @@ +
    + +
    +
      +
    • + +
    • +
    • + Cache timeout +
    • +
    • + +
    • +
    • + Max data points +
    • +
    • + +
    • +
    +
    +
    + +
    + +
    +
    + +
    +
    Shorter legend names
    +
      +
    • alias() function to specify a custom series name
    • +
    • aliasByNode(2) to alias by a specific part of your metric path
    • +
    • aliasByNode(2, -1) you can add multiple segment paths, and use negative index
    • +
    • groupByNode(2, 'sum') is useful if you have 2 wildcards in your metric path and want to sumSeries and group by
    • +
    +
    + +
    +
    Series as parameter
    +
      +
    • Some graphite functions allow you to have many series arguments
    • +
    • Use #[A-Z] to use a graphite query as parameter to a function
    • +
    • + Examples: +
        +
      • asPercent(#A, #B)
      • +
      • prod.srv-01.counters.count - asPercent(#A) : percentage of count in comparison with A query
      • +
      • prod.srv-01.counters.count - sumSeries(#A) : sum count and series A
      • +
      • divideSeries(#A, #B)
      • +
      +
    • +
    • If a query is added only to be used as a parameter, hide it from the graph with the eye icon
    • +
    +
    + +
    +
    Stacking
    +
      +
    • You find the stacking option under Display Styles tab
    • +
    • When stacking is enabled make sure null point mode is set to 'null as zero'
    • +
    +
    + +
    +
    Templating
    +
      +
    • You can use a template variable in place of metric names
    • +
    • You can use a template variable in place of function parameters
    • +
    • You enable the templating feature in Dashboard settings / Feature toggles
    • +
    +
    + +
    +
    Max data points
    +
      +
    • Every graphite request is issued with a maxDataPoints parameter
    • +
    • Graphite uses this parameter to consolidate the real number of values down to this number
    • +
    • If there are more real values, then by default they will be consolidated using averages
    • +
    • This could hide real peaks and max values in your series
    • +
    • You can change how point consolidation is made using the consolidateBy graphite function
    • +
    • Point consolidation will effect series legend values (min,max,total,current)
    • +
    • If you override maxDataPoint and set a high value performance can be severely effected
    • +
    +
    +
    +
    diff --git a/public/app/plugins/datasource/graphite/plugin.json b/public/app/plugins/datasource/graphite/plugin.json index 8e6766f087a..dd69d847bd4 100644 --- a/public/app/plugins/datasource/graphite/plugin.json +++ b/public/app/plugins/datasource/graphite/plugin.json @@ -8,9 +8,7 @@ "module": "plugins/datasource/graphite/datasource", "partials": { - "config": "app/plugins/datasource/graphite/partials/config.html", - "query": "app/plugins/datasource/graphite/partials/query.editor.html", - "annotations": "app/plugins/datasource/graphite/partials/annotations.editor.html" + "config": "app/plugins/datasource/graphite/partials/config.html" }, "metrics": true, diff --git a/public/app/plugins/datasource/graphite/queryCtrl.js b/public/app/plugins/datasource/graphite/queryCtrl.js index 040386e03d6..21b1534fbe1 100644 --- a/public/app/plugins/datasource/graphite/queryCtrl.js +++ b/public/app/plugins/datasource/graphite/queryCtrl.js @@ -9,15 +9,14 @@ function (angular, _, config, gfunc, Parser) { 'use strict'; var module = angular.module('grafana.controllers'); - var targetLetters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'; module.controller('GraphiteQueryCtrl', function($scope, $sce, templateSrv) { $scope.init = function() { - $scope.target.target = $scope.target.target || ''; - $scope.targetLetters = targetLetters; - - parseTarget(); + if ($scope.target) { + $scope.target.target = $scope.target.target || ''; + parseTarget(); + } }; $scope.toggleEditorMode = function() { @@ -313,22 +312,8 @@ function (angular, _, config, gfunc, Parser) { return new MetricSegment({value: 'select metric', fake: true}); }; - }); + $scope.init(); - module.directive('focusMe', function($timeout, $parse) { - return { - //scope: true, // optionally create a child scope - link: function(scope, element, attrs) { - var model = $parse(attrs.focusMe); - scope.$watch(model, function(value) { - if(value === true) { - $timeout(function() { - element[0].focus(); - }); - } - }); - } - }; }); }); diff --git a/public/app/plugins/datasource/influxdb/datasource.js b/public/app/plugins/datasource/influxdb/datasource.js index cca93fd4e3b..71714b7b40c 100644 --- a/public/app/plugins/datasource/influxdb/datasource.js +++ b/public/app/plugins/datasource/influxdb/datasource.js @@ -4,6 +4,7 @@ define([ 'kbn', './influxSeries', './queryBuilder', + './directives', './queryCtrl', './funcEditor', ], diff --git a/public/app/plugins/datasource/influxdb/directives.js b/public/app/plugins/datasource/influxdb/directives.js new file mode 100644 index 00000000000..a4c66137751 --- /dev/null +++ b/public/app/plugins/datasource/influxdb/directives.js @@ -0,0 +1,21 @@ +define([ + 'angular', +], +function (angular) { + 'use strict'; + + var module = angular.module('grafana.directives'); + + module.directive('metricQueryEditorInfluxdb', function() { + return {controller: 'InfluxQueryCtrl', templateUrl: 'app/plugins/datasource/influxdb/partials/query.editor.html'}; + }); + + module.directive('metricQueryOptionsInfluxdb', function() { + return {templateUrl: 'app/plugins/datasource/influxdb/partials/query.options.html'}; + }); + + module.directive('annotationsQueryEditorInfluxdb', function() { + return {templateUrl: 'app/plugins/datasource/influxdb/partials/annotations.editor.html'}; + }); + +}); diff --git a/public/app/plugins/datasource/influxdb/partials/query.editor.html b/public/app/plugins/datasource/influxdb/partials/query.editor.html index cbcfcb9f4c3..1ae2c037758 100644 --- a/public/app/plugins/datasource/influxdb/partials/query.editor.html +++ b/public/app/plugins/datasource/influxdb/partials/query.editor.html @@ -1,65 +1,56 @@ -
    - -
    -
    - +
    + +
  • + + + +
  • + - +
      +
    • + {{target.refId}} +
    • +
    • + + + +
    • +
    - + - +
      +
    • + {{target.refId}} +
    • +
    • + + + +
    • +
    -
    + +
    + +
    +
    + +
    + - -
    -
    - -
    - -
    -
    +
    +
    -
    -
    - -
    - -
    -
    - - +
    + + diff --git a/public/app/plugins/datasource/opentsdb/plugin.json b/public/app/plugins/datasource/opentsdb/plugin.json index dd7ab96d828..de0eb037d78 100644 --- a/public/app/plugins/datasource/opentsdb/plugin.json +++ b/public/app/plugins/datasource/opentsdb/plugin.json @@ -8,8 +8,7 @@ "module": "plugins/datasource/opentsdb/datasource", "partials": { - "config": "app/plugins/datasource/opentsdb/partials/config.html", - "query": "app/plugins/datasource/opentsdb/partials/query.editor.html" + "config": "app/plugins/datasource/opentsdb/partials/config.html" }, "metrics": true diff --git a/public/app/plugins/datasource/opentsdb/queryCtrl.js b/public/app/plugins/datasource/opentsdb/queryCtrl.js index 72ae9710d08..44c8496ca8a 100644 --- a/public/app/plugins/datasource/opentsdb/queryCtrl.js +++ b/public/app/plugins/datasource/opentsdb/queryCtrl.js @@ -113,6 +113,7 @@ function (angular, _, kbn) { return errs; } + $scope.init(); }); }); diff --git a/public/app/services/datasourceSrv.js b/public/app/services/datasourceSrv.js index 35256c9fe4f..644e84e8801 100644 --- a/public/app/services/datasourceSrv.js +++ b/public/app/services/datasourceSrv.js @@ -20,13 +20,24 @@ function (angular, _, config) { if (value.meta && value.meta.metrics) { self.metricSources.push({ value: key === config.defaultDatasource ? null : key, - name: key + name: key, + meta: value.meta, }); } if (value.meta && value.meta.annotations) { self.annotationSources.push(value); } }); + + this.metricSources.sort(function(a, b) { + if (a.meta.builtIn || a.name > b.name) { + return 1; + } + if (a.name < b.name) { + return -1; + } + return 0; + }); }; this.get = function(name) { diff --git a/public/css/less/bootswatch.dark.less b/public/css/less/bootswatch.dark.less index 4d83d04199c..ff15f4884ec 100644 --- a/public/css/less/bootswatch.dark.less +++ b/public/css/less/bootswatch.dark.less @@ -358,7 +358,6 @@ div.subnav { // BUTTONS // ----------------------------------------------------- - .btn { padding: 5px 12px; background-image: none; @@ -389,13 +388,6 @@ div.subnav { } .btn-group { - - & > .btn:first-child, - & > .btn:last-child, - & > .dropdown-toggle { - .border-radius(0); - } - & > .btn + .dropdown-toggle { .box-shadow(none); } diff --git a/public/css/less/tightform.less b/public/css/less/tightform.less index a85e852bcbc..7961cef775a 100644 --- a/public/css/less/tightform.less +++ b/public/css/less/tightform.less @@ -22,12 +22,14 @@ .tight-form-container-no-item-borders { border: 1px solid @grafanaTargetBorder; + border-bottom: none; .tight-form, .tight-form-item, [type=text].tight-form-input, [type=text].tight-form-clear-input { border: none; } } + .spaced-form { .tight-form { margin: 7px 0; @@ -42,12 +44,11 @@ } .tight-form-container { + border-bottom: 1px solid @grafanaTargetBorder; + .tight-form:last-child { border-bottom: none; } - &:last-child { - border-bottom: 1px solid @grafanaTargetBorder; - } } .tight-form-btn { @@ -63,7 +64,7 @@ } .grafana-metric-options { - margin-top: 35px; + margin-top: 25px; } .tight-form-item { @@ -209,3 +210,7 @@ select.tight-form-input { } } +.tight-form-align { + padding-left: 66px; +} + diff --git a/public/test/specs/graphiteTargetCtrl-specs.js b/public/test/specs/graphiteTargetCtrl-specs.js index 31916cc2802..9680ffb5742 100644 --- a/public/test/specs/graphiteTargetCtrl-specs.js +++ b/public/test/specs/graphiteTargetCtrl-specs.js @@ -13,9 +13,7 @@ define([ beforeEach(ctx.createControllerPhase('GraphiteQueryCtrl')); beforeEach(function() { - ctx.scope.target = { - target: 'aliasByNode(scaleToSeconds(test.prod.*,1),2)' - }; + ctx.scope.target = {target: 'aliasByNode(scaleToSeconds(test.prod.*,1),2)'}; ctx.scope.datasource = ctx.datasource; ctx.scope.datasource.metricFindQuery = sinon.stub().returns(ctx.$q.when([])); diff --git a/public/test/specs/panelSrv-specs.js b/public/test/specs/panelSrv-specs.js new file mode 100644 index 00000000000..52e82379db3 --- /dev/null +++ b/public/test/specs/panelSrv-specs.js @@ -0,0 +1,58 @@ +define([ + 'helpers', + 'features/panel/panelSrv', +], function() { + 'use strict'; + + describe('PanelSrv', function() { + var _panelSrv; + var _panelScope; + var _datasourceSrvStub; + + beforeEach(module('grafana.services')); + beforeEach(module(function($provide) { + _datasourceSrvStub = { + getMetricSources: sinon.spy(), + }; + $provide.value('datasourceSrv', _datasourceSrvStub); + })); + + beforeEach(inject(function(panelSrv, $rootScope) { + _panelSrv = panelSrv; + _panelScope = $rootScope.$new(); + _panelScope.panel = { + targets: [], + }; + _panelScope.dashboardViewState = { + registerPanel: sinon.spy(), + }; + })); + + describe('init', function() { + beforeEach(function() { + _panelSrv.init(_panelScope); + }); + + describe('addDataQuery', function() { + it('should add target', function() { + _panelScope.addDataQuery(); + expect(_panelScope.panel.targets.length).to.be(1); + }); + + it('should set refId', function() { + _panelScope.addDataQuery(); + expect(_panelScope.panel.targets[0].refId).to.be('A'); + }); + + it('should set refId to first available letter', function() { + _panelScope.panel.targets = [{refId: 'A'}]; + _panelScope.addDataQuery(); + expect(_panelScope.panel.targets[1].refId).to.be('B'); + }); + }); + + }); + }); + +}); + diff --git a/public/test/test-main.js b/public/test/test-main.js index 946cc6aad77..76bc316658a 100644 --- a/public/test/test-main.js +++ b/public/test/test-main.js @@ -139,6 +139,7 @@ require([ 'specs/seriesOverridesCtrl-specs', 'specs/shareModalCtrl-specs', 'specs/timeSrv-specs', + 'specs/panelSrv-specs', 'specs/templateSrv-specs', 'specs/templateValuesSrv-specs', 'specs/kbn-format-specs', diff --git a/public/vendor/bootstrap/less/bootstrap.less b/public/vendor/bootstrap/less/bootstrap.less index 758dbde9848..9529c54cda4 100644 --- a/public/vendor/bootstrap/less/bootstrap.less +++ b/public/vendor/bootstrap/less/bootstrap.less @@ -32,6 +32,7 @@ // Components: Buttons & Alerts @import "buttons.less"; +@import "button-groups.less"; @import "alerts.less"; // Note: alerts share common CSS with buttons and thus have styles in buttons.less // Components: Nav