Просмотр исходного кода

Merge branch 'ds-template-var'

Torkel Ödegaard 9 лет назад
Родитель
Сommit
0c9a19e38c

+ 1 - 0
CHANGELOG.md

@@ -2,6 +2,7 @@
 
 ### Enhancements
 * **Singlestat**: Support for gauges in singlestat panel. closes [#3688](https://github.com/grafana/grafana/pull/3688)
+* **Templating**: Support for data source as variable, closes [#816](https://github.com/grafana/grafana/pull/816)
 
 ### Bug fixes
 * **InfluxDB 0.12**: Fixed issue templating and `show tag values` query only returning tags for first measurement,  fixes [#4726](https://github.com/grafana/grafana/issues/4726)

+ 1 - 1
public/app/core/components/info_popover.ts

@@ -11,7 +11,6 @@ export function infoPopover() {
     template: '<i class="fa fa-info-circle"></i>',
     transclude: true,
     link: function(scope, elem, attrs, ctrl, transclude) {
-
       var offset = attrs.offset || '0 -10px';
       var position = attrs.position || 'right middle';
       var classes = 'drop-help drop-hide-out-of-bounds';
@@ -39,6 +38,7 @@ export function infoPopover() {
           position: position,
           classes: classes,
           openOn: openOn,
+          hoverOpenDelay: 400,
           tetherOptions: {
             offset: offset
           }

+ 56 - 29
public/app/core/services/datasource_srv.js

@@ -7,36 +7,11 @@ define([
 function (angular, _, coreModule, config) {
   'use strict';
 
-  coreModule.default.service('datasourceSrv', function($q, $injector, $rootScope) {
+  coreModule.default.service('datasourceSrv', function($q, $injector, $rootScope, templateSrv) {
     var self = this;
 
     this.init = function() {
       this.datasources = {};
-      this.metricSources = [];
-      this.annotationSources = [];
-
-      _.each(config.datasources, function(value, key) {
-        if (value.meta && value.meta.metrics) {
-          self.metricSources.push({
-            value: key === config.defaultDatasource ? null : 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) {
@@ -44,6 +19,8 @@ function (angular, _, coreModule, config) {
         return this.get(config.defaultDatasource);
       }
 
+      name = templateSrv.replace(name);
+
       if (this.datasources[name]) {
         return $q.when(this.datasources[name]);
       }
@@ -89,11 +66,61 @@ function (angular, _, coreModule, config) {
     };
 
     this.getAnnotationSources = function() {
-      return this.annotationSources;
+      return _.reduce(config.datasources, function(memo, key, value) {
+
+        if (value.meta && value.meta.annotations) {
+          memo.push(value);
+        }
+
+        return memo;
+      }, []);
     };
 
-    this.getMetricSources = function() {
-      return this.metricSources;
+    this.getMetricSources = function(options) {
+      var metricSources = [];
+
+      _.each(config.datasources, function(value, key) {
+        if (value.meta && value.meta.metrics) {
+          metricSources.push({
+            value: key === config.defaultDatasource ? null : key,
+            name: key,
+            meta: value.meta,
+          });
+        }
+      });
+
+      if (!options || !options.skipVariables) {
+        // look for data source variables
+        for (var i = 0; i < templateSrv.variables.length; i++) {
+          var variable = templateSrv.variables[i];
+          if (variable.type !== 'datasource') {
+            continue;
+          }
+
+          var first = variable.current.value;
+          var ds = config.datasources[first];
+
+          if (ds) {
+            metricSources.push({
+              name: '$' + variable.name,
+              value: '$' + variable.name,
+              meta: ds.meta,
+            });
+          }
+        }
+      }
+
+      metricSources.sort(function(a, b) {
+        if (a.meta.builtIn || a.name > b.name) {
+          return 1;
+        }
+        if (a.name < b.name) {
+          return -1;
+        }
+        return 0;
+      });
+
+      return metricSources;
     };
 
     this.init();

+ 1 - 1
public/app/core/services/segment_srv.js

@@ -19,7 +19,7 @@ function (angular, _, coreModule) {
 
       if (_.isString(options)) {
         this.value = options;
-        this.html = $sce.trustAsHtml(this.value);
+        this.html = $sce.trustAsHtml(templateSrv.highlightVariablesAsHtml(this.value));
         return;
       }
 

+ 4 - 0
public/app/features/panel/metrics_ds_selector.ts

@@ -63,6 +63,10 @@ export class MetricsDsSelectorCtrl {
       }
     }
 
+    if (!this.current) {
+      this.current = {name: dsValue + ' not found', value: null};
+    }
+
     this.dsSegment = uiSegmentSrv.newSegment(this.current.name);
   }
 

+ 2 - 0
public/app/features/panel/metrics_panel_ctrl.ts

@@ -15,6 +15,7 @@ class MetricsPanelCtrl extends PanelCtrl {
   error: boolean;
   loading: boolean;
   datasource: any;
+  datasourceName: any;
   $q: any;
   $timeout: any;
   datasourceSrv: any;
@@ -244,6 +245,7 @@ class MetricsPanelCtrl extends PanelCtrl {
     }
 
     this.panel.datasource = datasource.value;
+    this.datasourceName = datasource.name;
     this.datasource = null;
     this.refresh();
   }

+ 20 - 0
public/app/features/templating/editorCtrl.js

@@ -20,6 +20,13 @@ function (angular, _) {
       multi: false,
     };
 
+    $scope.variableTypes = [
+      {value: "query",      text: "Query"},
+      {value: "interval",   text: "Interval"},
+      {value: "datasource", text: "Datasource"},
+      {value: "custom",     text: "Custom"},
+    ];
+
     $scope.refreshOptions = [
       {value: 0, text: "Never"},
       {value: 1, text: "On Dashboard Load"},
@@ -35,10 +42,16 @@ function (angular, _) {
     $scope.init = function() {
       $scope.mode = 'list';
 
+      $scope.datasourceTypes = {};
       $scope.datasources = _.filter(datasourceSrv.getMetricSources(), function(ds) {
+        $scope.datasourceTypes[ds.meta.id] = {text: ds.meta.name, value: ds.meta.id};
         return !ds.meta.builtIn;
       });
 
+      $scope.datasourceTypes = _.map($scope.datasourceTypes, function(value) {
+        return value;
+      });
+
       $scope.variables = templateSrv.variables;
       $scope.reset();
 
@@ -132,9 +145,16 @@ function (angular, _) {
       if ($scope.current.type === 'interval') {
         $scope.current.query = '1m,10m,30m,1h,6h,12h,1d,7d,14d,30d';
       }
+
       if ($scope.current.type === 'query') {
         $scope.current.query = '';
       }
+
+      if ($scope.current.type === 'datasource') {
+        $scope.current.query = $scope.datasourceTypes[0].value;
+        $scope.current.regex = '';
+        $scope.current.refresh = 1;
+      }
     };
 
     $scope.removeVariable = function(variable) {

+ 92 - 55
public/app/features/templating/partials/editor.html

@@ -75,39 +75,49 @@
 			<div class="gf-form-group">
 				<div class="gf-form-inline">
 					<div class="gf-form max-width-19">
-						<span class="gf-form-label width-7">Name</span>
-						<input type="text" class="gf-form-input max-width-12" placeholder="name" ng-model='current.name'></input>
-					</div>
-					<div class="gf-form">
-						<span class="gf-form-label width-4">Type</span>
-						<div class="gf-form-select-wrapper">
-							<select class="gf-form-input width-7" ng-model="current.type" ng-options="f for f in ['query', 'interval', 'custom']" ng-change="typeChanged()"></select>
-						</div>
+						<span class="gf-form-label width-6">Name</span>
+						<input type="text" class="gf-form-input" placeholder="name" ng-model='current.name'></input>
 					</div>
 					<div class="gf-form max-width-19">
-						<span class="gf-form-label width-7" ng-show="current.type === 'query'">Data source</span>
-						<div class="gf-form-select-wrapper max-width-12" ng-show="current.type === 'query'">
-							<select class="gf-form-input" ng-model="current.datasource" ng-options="f.value as f.name for f in datasources"></select>
+						<span class="gf-form-label width-6">
+              Type
+              <info-popover mode="right-normal">
+                <dl>
+                  <dt>Query</dt>
+                  <dd>Variable values are fetched from a metric names query to a data source</dd>
+                  <dt>Interval</dt>
+                  <dd>Timespan variable type</dd>
+                  <dt>Datasource</dt>
+                  <dd>Dynamically switch data sources using this type of variable</dd>
+                  <dt>Custom</dt>
+                  <dd>Define variable values manually</dd>
+                </dl>
+                <a href="http://docs.grafana.org/reference/templating" target="_blank">Templating docs</a>
+              </info-popover>
+            </span>
+						<div class="gf-form-select-wrapper max-width-17">
+							<select class="gf-form-input" ng-model="current.type" ng-options="f.value as f.text for f in variableTypes" ng-change="typeChanged()"></select>
 						</div>
 					</div>
-				</div>
+        </div>
 				<div class="gf-form-inline">
 					<div class="gf-form max-width-19">
-						<span class="gf-form-label width-7">Label</span>
-						<input type="text" class="gf-form-input max-width-12" ng-model='current.label' placeholder="optional display name"></input>
+						<span class="gf-form-label width-6">Label</span>
+						<input type="text" class="gf-form-input" ng-model='current.label' placeholder="optional display name"></input>
 					</div>
-					<div class="gf-form">
-						<span class="gf-form-label width-4">Hide</span>
-            <div class="gf-form-select-wrapper">
-							<select class="gf-form-input width-7" ng-model="current.hide" ng-options="f.value as f.text for f in hideOptions"></select>
+					<div class="gf-form max-width-19">
+						<span class="gf-form-label width-6">Hide</span>
+            <div class="gf-form-select-wrapper max-width-15">
+							<select class="gf-form-input" ng-model="current.hide" ng-options="f.value as f.text for f in hideOptions"></select>
 						</div>
 					</div>
 				</div>
 
 			</div>
 
-			<h5 class="section-heading">Value Options</h5>
 			<div ng-show="current.type === 'interval'" class="gf-form-group">
+        <h5 class="section-heading">Interval Options</h5>
+
 				<div class="gf-form">
 					<span class="gf-form-label width-9">Values</span>
 					<input type="text" class="gf-form-input" placeholder="name" ng-model='current.query' placeholder="1m,10m,1h,6h,1d,7d" ng-model-onblur ng-change="runQuery()"></input>
@@ -135,6 +145,7 @@
 			</div>
 
 			<div ng-show="current.type === 'custom'" class="gf-form-group">
+        <h5 class="section-heading">Custom Options</h5>
 				<div class="gf-form">
 					<span class="gf-form-label width-13">Values seperated by comma</span>
 					<input type="text" class="gf-form-input" ng-model='current.query' ng-blur="runQuery()" placeholder="1, 10, 20, myvalue"></input>
@@ -142,43 +153,69 @@
 			</div>
 
 			<div ng-show="current.type === 'query'" class="gf-form-group">
-				<div class="gf-form">
-					<span class="gf-form-label width-7">Query</span>
-					<input type="text" class="gf-form-input" ng-model='current.query' placeholder="metric name or tags query" ng-model-onblur ng-change="runQuery()"></input>
-					<!-- <info&#45;popover position="bottom center" wide="true"> -->
-					<!-- 	Example queries: -->
-					<!-- 	<ul> -->
-					<!-- 		<li> -->
-					<!-- 			<code>SHOW TAG VALUES WITH KEY = "hostname"</code> -->
-					<!-- 		</li> -->
-					<!-- 		<li> -->
-					<!-- 			<code>SHOW TAG VALUES WITH KEY = "hostname"</code> -->
-					<!-- 		</li> -->
-					<!-- 		<li> -->
-					<!-- 			<code>SHOW TAG VALUES WITH KEY = "hostname"</code> -->
-					<!-- 		</li> -->
-					<!-- 		<li> -->
-					<!-- 			<a href="http://docs.grafana.org" target="_blank">Templating docs</a> -->
-					<!-- 		</li> -->
-					<!-- 	</ul> -->
-					<!-- </info&#45;popover> -->
-				</div>
-				<div class="gf-form">
-					<span class="gf-form-label width-7">
-						Regex
-						<tip>Optional, if you want to extract part of a series name or metric node segment</tip>
-					</span>
-					<input type="text" class="gf-form-input" ng-model='current.regex' placeholder="/.*-(.*)-.*/" ng-model-onblur ng-change="runQuery()"></input>
-				</div>
-				<div class="gf-form">
-					<span class="gf-form-label width-7">Refresh</span>
-					<select class="gf-form-input max-width-14" ng-model="current.refresh" ng-options="f.value as f.text for f in refreshOptions"></select>
-					<tip>When to update the values of this variable, will slow down dashboard load / time change</tip>
-				</div>
-			</div>
+        <h5 class="section-heading">Query Options</h5>
+
+        <div class="gf-form-inline">
+          <div class="gf-form max-width-21">
+            <span class="gf-form-label width-7" ng-show="current.type === 'query'">Data source</span>
+            <div class="gf-form-select-wrapper max-width-14">
+              <select class="gf-form-input" ng-model="current.datasource" ng-options="f.value as f.name for f in datasources"></select>
+            </div>
+          </div>
+          <div class="gf-form max-width-21">
+            <span class="gf-form-label width-7">
+              Refresh
+              <info-popover mode="right-normal">
+                When to update the values of this variable.
+              </info-popover>
+            </span>
+            <div class="gf-form-select-wrapper max-width-14">
+              <select class="gf-form-input" ng-model="current.refresh" ng-options="f.value as f.text for f in refreshOptions"></select>
+            </div>
+          </div>
+        </div>
+        <div class="gf-form">
+          <span class="gf-form-label width-7">Query</span>
+          <input type="text" class="gf-form-input" ng-model='current.query' placeholder="metric name or tags query" ng-model-onblur ng-change="runQuery()"></input>
+        </div>
+        <div class="gf-form">
+          <span class="gf-form-label width-7">
+            Regex
+            <info-popover mode="right-normal">
+              Optional, if you want to extract part of a series name or metric node segment.
+            </info-popover>
+          </span>
+          <input type="text" class="gf-form-input" ng-model='current.regex' placeholder="/.*-(.*)-.*/" ng-model-onblur ng-change="runQuery()"></input>
+        </div>
+      </div>
+
+      <div ng-show="current.type === 'datasource'" class="gf-form-group">
+        <h5 class="section-heading">Datasource Options</h5>
+
+        <div class="gf-form">
+          <label class="gf-form-label width-12">Type</label>
+          <div class="gf-form-select-wrapper max-width-18">
+            <select class="gf-form-input" ng-model="current.query" ng-options="f.value as f.text for f in datasourceTypes" ng-change="runQuery()"></select>
+          </div>
+        </div>
+
+        <div class="gf-form">
+          <label class="gf-form-label width-12">
+            Instance name filter
+            <info-popover mode="right-normal">
+              Regex filter for which data source instances to choose from in
+              the variable value dropdown. Leave empty for all.
+              <br><br>
+              Example: <code>/^prod/</code>
+
+            </info-popover>
+          </label>
+          <input type="text" class="gf-form-input max-width-18" ng-model='current.regex' placeholder="/.*-(.*)-.*/" ng-model-onblur ng-change="runQuery()"></input>
+        </div>
+      </div>
 
-			<div class="section gf-form-group" >
-				<h5 class="section-heading">Selection Options</h5>
+      <div class="section gf-form-group" ng-hide="current.type === 'datasource'">
+        <h5 class="section-heading">Selection Options</h5>
         <div class="section">
           <gf-form-switch class="gf-form"
                           label="Multi-value"

+ 40 - 2
public/app/features/templating/templateValuesSrv.js

@@ -63,7 +63,9 @@ function (angular, _, kbn) {
       // determine our dependencies.
       if (variable.type === "query") {
         _.forEach(this.variables, function(v) {
-          if (templateSrv.containsVariable(variable.query, v.name)) {
+          // both query and datasource can contain variable
+          if (templateSrv.containsVariable(variable.query, v.name) ||
+              templateSrv.containsVariable(variable.datasource, v.name)) {
             dependencies.push(self.variableLock[v.name].promise);
           }
         });
@@ -149,7 +151,8 @@ function (angular, _, kbn) {
         if (otherVariable === updatedVariable) {
           return;
         }
-        if (templateSrv.containsVariable(otherVariable.query, updatedVariable.name)) {
+        if (templateSrv.containsVariable(otherVariable.query, updatedVariable.name) ||
+            templateSrv.containsVariable(otherVariable.datasource, updatedVariable.name)) {
           return self.updateOptions(otherVariable);
         }
       });
@@ -158,6 +161,11 @@ function (angular, _, kbn) {
     };
 
     this._updateNonQueryVariable = function(variable) {
+      if (variable.type === 'datasource') {
+        self.updateDataSourceVariable(variable);
+        return;
+      }
+
       // extract options in comma seperated string
       variable.options = _.map(variable.query.split(/[,]+/), function(text) {
         return { text: text.trim(), value: text.trim() };
@@ -172,6 +180,36 @@ function (angular, _, kbn) {
       }
     };
 
+    this.updateDataSourceVariable = function(variable) {
+      var options = [];
+      var sources = datasourceSrv.getMetricSources({skipVariables: true});
+      var regex;
+
+      if (variable.regex) {
+        regex = kbn.stringToJsRegex(templateSrv.replace(variable.regex));
+      }
+
+      for (var i = 0; i < sources.length; i++) {
+        var source = sources[i];
+        // must match on type
+        if (source.meta.id !== variable.query) {
+          continue;
+        }
+
+        if (regex && !regex.exec(source.name)) {
+          continue;
+        }
+
+        options.push({text: source.name, value: source.name});
+      }
+
+      if (options.length === 0) {
+        options.push({text: 'No datasurces found', value: ''});
+      }
+
+      variable.options = options;
+    };
+
     this.updateOptions = function(variable) {
       if (variable.type !== 'query') {
         self._updateNonQueryVariable(variable);

+ 2 - 4
public/sass/base/_code.scss

@@ -11,15 +11,12 @@ pre {
   background-color: $code-tag-bg;
   color: $text-color;
   border: 1px solid $code-tag-border;
-  padding: 10px;
   border-radius: 4px;
 }
 
 // Inline code
 code {
   color: $text-color;
-  background-color: $code-tag-bg;
-  border: 1px solid darken($code-tag-bg, 15%);
   white-space: nowrap;
   padding: 2px 5px;
   margin: 0 2px;
@@ -27,7 +24,7 @@ code {
 
 code.code--small {
   font-size: $font-size-xs;
-  padding: 5px;
+  padding: 0.2rem;
   margin: 0 2px;
 }
 
@@ -41,6 +38,7 @@ pre {
   white-space: pre;
   white-space: pre-wrap;
   background-color: $code-tag-bg;
+  padding: 10px;
 
   // Make prettyprint styles more spaced out for readability
   &.prettyprint {

+ 20 - 3
public/test/specs/templateValuesSrv-specs.js

@@ -92,6 +92,7 @@ define([
           var ds = {};
           ds.metricFindQuery = sinon.stub().returns(ctx.$q.when(scenario.queryResult));
           ctx.datasourceSrv.get = sinon.stub().returns(ctx.$q.when(ds));
+          ctx.datasourceSrv.getMetricSources = sinon.stub().returns(scenario.metricSources);
 
           ctx.service.updateOptions(scenario.variable);
           ctx.$rootScope.$digest();
@@ -137,7 +138,6 @@ define([
       });
     });
 
-
     describeUpdateVariable('interval variable with auto', function(scenario) {
       scenario.setup(function() {
         scenario.variable = { type: 'interval', query: '1s,2h,5h,1d', name: 'test', auto: true, auto_count: 10 };
@@ -237,7 +237,7 @@ define([
       });
     });
 
-   describeUpdateVariable('regex pattern without slashes', function(scenario) {
+    describeUpdateVariable('regex pattern without slashes', function(scenario) {
       scenario.setup(function() {
         scenario.variable = { type: 'query', query: 'apps.*', name: 'test' };
         scenario.variable.regex = 'backend_01';
@@ -284,6 +284,23 @@ define([
       });
     });
 
-  });
+    describeUpdateVariable('datasource variable with regex filter', function(scenario) {
+      scenario.setup(function() {
+        scenario.variable = {type: 'datasource', query: 'graphite', name: 'test', current: {}, regex: '/pee$/' };
+        scenario.metricSources = [
+          {name: 'backend1', meta: {id: 'influx'}},
+          {name: 'backend2_pee', meta: {id: 'graphite'}},
+          {name: 'backend3', meta: {id: 'graphite'}},
+          {name: 'backend4_pee', meta: {id: 'graphite'}},
+        ];
+      });
 
+      it('should set only contain graphite ds and filtered using regex', function() {
+        expect(scenario.variable.options.length).to.be(2);
+        expect(scenario.variable.options[0].value).to.be('backend2_pee');
+        expect(scenario.variable.options[1].value).to.be('backend4_pee');
+      });
+    });
+
+  });
 });