Commit 0c9a19e3 by Torkel Ödegaard

Merge branch 'ds-template-var'

parents c4440eab 8b4c7c94
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
### Enhancements ### Enhancements
* **Singlestat**: Support for gauges in singlestat panel. closes [#3688](https://github.com/grafana/grafana/pull/3688) * **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 ### 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) * **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)
......
...@@ -11,7 +11,6 @@ export function infoPopover() { ...@@ -11,7 +11,6 @@ export function infoPopover() {
template: '<i class="fa fa-info-circle"></i>', template: '<i class="fa fa-info-circle"></i>',
transclude: true, transclude: true,
link: function(scope, elem, attrs, ctrl, transclude) { link: function(scope, elem, attrs, ctrl, transclude) {
var offset = attrs.offset || '0 -10px'; var offset = attrs.offset || '0 -10px';
var position = attrs.position || 'right middle'; var position = attrs.position || 'right middle';
var classes = 'drop-help drop-hide-out-of-bounds'; var classes = 'drop-help drop-hide-out-of-bounds';
...@@ -39,6 +38,7 @@ export function infoPopover() { ...@@ -39,6 +38,7 @@ export function infoPopover() {
position: position, position: position,
classes: classes, classes: classes,
openOn: openOn, openOn: openOn,
hoverOpenDelay: 400,
tetherOptions: { tetherOptions: {
offset: offset offset: offset
} }
......
...@@ -7,36 +7,11 @@ define([ ...@@ -7,36 +7,11 @@ define([
function (angular, _, coreModule, config) { function (angular, _, coreModule, config) {
'use strict'; 'use strict';
coreModule.default.service('datasourceSrv', function($q, $injector, $rootScope) { coreModule.default.service('datasourceSrv', function($q, $injector, $rootScope, templateSrv) {
var self = this; var self = this;
this.init = function() { this.init = function() {
this.datasources = {}; 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) { this.get = function(name) {
...@@ -44,6 +19,8 @@ function (angular, _, coreModule, config) { ...@@ -44,6 +19,8 @@ function (angular, _, coreModule, config) {
return this.get(config.defaultDatasource); return this.get(config.defaultDatasource);
} }
name = templateSrv.replace(name);
if (this.datasources[name]) { if (this.datasources[name]) {
return $q.when(this.datasources[name]); return $q.when(this.datasources[name]);
} }
...@@ -89,11 +66,61 @@ function (angular, _, coreModule, config) { ...@@ -89,11 +66,61 @@ function (angular, _, coreModule, config) {
}; };
this.getAnnotationSources = function() { 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() { this.getMetricSources = function(options) {
return this.metricSources; 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(); this.init();
......
...@@ -19,7 +19,7 @@ function (angular, _, coreModule) { ...@@ -19,7 +19,7 @@ function (angular, _, coreModule) {
if (_.isString(options)) { if (_.isString(options)) {
this.value = options; this.value = options;
this.html = $sce.trustAsHtml(this.value); this.html = $sce.trustAsHtml(templateSrv.highlightVariablesAsHtml(this.value));
return; return;
} }
......
...@@ -63,6 +63,10 @@ export class MetricsDsSelectorCtrl { ...@@ -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); this.dsSegment = uiSegmentSrv.newSegment(this.current.name);
} }
......
...@@ -15,6 +15,7 @@ class MetricsPanelCtrl extends PanelCtrl { ...@@ -15,6 +15,7 @@ class MetricsPanelCtrl extends PanelCtrl {
error: boolean; error: boolean;
loading: boolean; loading: boolean;
datasource: any; datasource: any;
datasourceName: any;
$q: any; $q: any;
$timeout: any; $timeout: any;
datasourceSrv: any; datasourceSrv: any;
...@@ -244,6 +245,7 @@ class MetricsPanelCtrl extends PanelCtrl { ...@@ -244,6 +245,7 @@ class MetricsPanelCtrl extends PanelCtrl {
} }
this.panel.datasource = datasource.value; this.panel.datasource = datasource.value;
this.datasourceName = datasource.name;
this.datasource = null; this.datasource = null;
this.refresh(); this.refresh();
} }
......
...@@ -20,6 +20,13 @@ function (angular, _) { ...@@ -20,6 +20,13 @@ function (angular, _) {
multi: false, multi: false,
}; };
$scope.variableTypes = [
{value: "query", text: "Query"},
{value: "interval", text: "Interval"},
{value: "datasource", text: "Datasource"},
{value: "custom", text: "Custom"},
];
$scope.refreshOptions = [ $scope.refreshOptions = [
{value: 0, text: "Never"}, {value: 0, text: "Never"},
{value: 1, text: "On Dashboard Load"}, {value: 1, text: "On Dashboard Load"},
...@@ -35,10 +42,16 @@ function (angular, _) { ...@@ -35,10 +42,16 @@ function (angular, _) {
$scope.init = function() { $scope.init = function() {
$scope.mode = 'list'; $scope.mode = 'list';
$scope.datasourceTypes = {};
$scope.datasources = _.filter(datasourceSrv.getMetricSources(), function(ds) { $scope.datasources = _.filter(datasourceSrv.getMetricSources(), function(ds) {
$scope.datasourceTypes[ds.meta.id] = {text: ds.meta.name, value: ds.meta.id};
return !ds.meta.builtIn; return !ds.meta.builtIn;
}); });
$scope.datasourceTypes = _.map($scope.datasourceTypes, function(value) {
return value;
});
$scope.variables = templateSrv.variables; $scope.variables = templateSrv.variables;
$scope.reset(); $scope.reset();
...@@ -132,9 +145,16 @@ function (angular, _) { ...@@ -132,9 +145,16 @@ function (angular, _) {
if ($scope.current.type === 'interval') { if ($scope.current.type === 'interval') {
$scope.current.query = '1m,10m,30m,1h,6h,12h,1d,7d,14d,30d'; $scope.current.query = '1m,10m,30m,1h,6h,12h,1d,7d,14d,30d';
} }
if ($scope.current.type === 'query') { if ($scope.current.type === 'query') {
$scope.current.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) { $scope.removeVariable = function(variable) {
......
...@@ -75,39 +75,49 @@ ...@@ -75,39 +75,49 @@
<div class="gf-form-group"> <div class="gf-form-group">
<div class="gf-form-inline"> <div class="gf-form-inline">
<div class="gf-form max-width-19"> <div class="gf-form max-width-19">
<span class="gf-form-label width-7">Name</span> <span class="gf-form-label width-6">Name</span>
<input type="text" class="gf-form-input max-width-12" placeholder="name" ng-model='current.name'></input> <input type="text" class="gf-form-input" 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>
</div> </div>
<div class="gf-form max-width-19"> <div class="gf-form max-width-19">
<span class="gf-form-label width-7" ng-show="current.type === 'query'">Data source</span> <span class="gf-form-label width-6">
<div class="gf-form-select-wrapper max-width-12" ng-show="current.type === 'query'"> Type
<select class="gf-form-input" ng-model="current.datasource" ng-options="f.value as f.name for f in datasources"></select> <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> </div>
<div class="gf-form-inline"> <div class="gf-form-inline">
<div class="gf-form max-width-19"> <div class="gf-form max-width-19">
<span class="gf-form-label width-7">Label</span> <span class="gf-form-label width-6">Label</span>
<input type="text" class="gf-form-input max-width-12" ng-model='current.label' placeholder="optional display name"></input> <input type="text" class="gf-form-input" ng-model='current.label' placeholder="optional display name"></input>
</div> </div>
<div class="gf-form"> <div class="gf-form max-width-19">
<span class="gf-form-label width-4">Hide</span> <span class="gf-form-label width-6">Hide</span>
<div class="gf-form-select-wrapper"> <div class="gf-form-select-wrapper max-width-15">
<select class="gf-form-input width-7" ng-model="current.hide" ng-options="f.value as f.text for f in hideOptions"></select> <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>
</div> </div>
</div> </div>
<h5 class="section-heading">Value Options</h5>
<div ng-show="current.type === 'interval'" class="gf-form-group"> <div ng-show="current.type === 'interval'" class="gf-form-group">
<h5 class="section-heading">Interval Options</h5>
<div class="gf-form"> <div class="gf-form">
<span class="gf-form-label width-9">Values</span> <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> <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 @@ ...@@ -135,6 +145,7 @@
</div> </div>
<div ng-show="current.type === 'custom'" class="gf-form-group"> <div ng-show="current.type === 'custom'" class="gf-form-group">
<h5 class="section-heading">Custom Options</h5>
<div class="gf-form"> <div class="gf-form">
<span class="gf-form-label width-13">Values seperated by comma</span> <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> <input type="text" class="gf-form-input" ng-model='current.query' ng-blur="runQuery()" placeholder="1, 10, 20, myvalue"></input>
...@@ -142,42 +153,68 @@ ...@@ -142,42 +153,68 @@
</div> </div>
<div ng-show="current.type === 'query'" class="gf-form-group"> <div ng-show="current.type === 'query'" class="gf-form-group">
<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"> <div class="gf-form">
<span class="gf-form-label width-7">Query</span> <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> <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>
<div class="gf-form"> <div class="gf-form">
<span class="gf-form-label width-7"> <span class="gf-form-label width-7">
Regex Regex
<tip>Optional, if you want to extract part of a series name or metric node segment</tip> <info-popover mode="right-normal">
Optional, if you want to extract part of a series name or metric node segment.
</info-popover>
</span> </span>
<input type="text" class="gf-form-input" ng-model='current.regex' placeholder="/.*-(.*)-.*/" ng-model-onblur ng-change="runQuery()"></input> <input type="text" class="gf-form-input" ng-model='current.regex' placeholder="/.*-(.*)-.*/" ng-model-onblur ng-change="runQuery()"></input>
</div> </div>
</div>
<div ng-show="current.type === 'datasource'" class="gf-form-group">
<h5 class="section-heading">Datasource Options</h5>
<div class="gf-form"> <div class="gf-form">
<span class="gf-form-label width-7">Refresh</span> <label class="gf-form-label width-12">Type</label>
<select class="gf-form-input max-width-14" ng-model="current.refresh" ng-options="f.value as f.text for f in refreshOptions"></select> <div class="gf-form-select-wrapper max-width-18">
<tip>When to update the values of this variable, will slow down dashboard load / time change</tip> <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> </div>
<div class="section gf-form-group" > <div class="section gf-form-group" ng-hide="current.type === 'datasource'">
<h5 class="section-heading">Selection Options</h5> <h5 class="section-heading">Selection Options</h5>
<div class="section"> <div class="section">
<gf-form-switch class="gf-form" <gf-form-switch class="gf-form"
......
...@@ -63,7 +63,9 @@ function (angular, _, kbn) { ...@@ -63,7 +63,9 @@ function (angular, _, kbn) {
// determine our dependencies. // determine our dependencies.
if (variable.type === "query") { if (variable.type === "query") {
_.forEach(this.variables, function(v) { _.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); dependencies.push(self.variableLock[v.name].promise);
} }
}); });
...@@ -149,7 +151,8 @@ function (angular, _, kbn) { ...@@ -149,7 +151,8 @@ function (angular, _, kbn) {
if (otherVariable === updatedVariable) { if (otherVariable === updatedVariable) {
return; 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); return self.updateOptions(otherVariable);
} }
}); });
...@@ -158,6 +161,11 @@ function (angular, _, kbn) { ...@@ -158,6 +161,11 @@ function (angular, _, kbn) {
}; };
this._updateNonQueryVariable = function(variable) { this._updateNonQueryVariable = function(variable) {
if (variable.type === 'datasource') {
self.updateDataSourceVariable(variable);
return;
}
// extract options in comma seperated string // extract options in comma seperated string
variable.options = _.map(variable.query.split(/[,]+/), function(text) { variable.options = _.map(variable.query.split(/[,]+/), function(text) {
return { text: text.trim(), value: text.trim() }; return { text: text.trim(), value: text.trim() };
...@@ -172,6 +180,36 @@ function (angular, _, kbn) { ...@@ -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) { this.updateOptions = function(variable) {
if (variable.type !== 'query') { if (variable.type !== 'query') {
self._updateNonQueryVariable(variable); self._updateNonQueryVariable(variable);
......
...@@ -11,15 +11,12 @@ pre { ...@@ -11,15 +11,12 @@ pre {
background-color: $code-tag-bg; background-color: $code-tag-bg;
color: $text-color; color: $text-color;
border: 1px solid $code-tag-border; border: 1px solid $code-tag-border;
padding: 10px;
border-radius: 4px; border-radius: 4px;
} }
// Inline code // Inline code
code { code {
color: $text-color; color: $text-color;
background-color: $code-tag-bg;
border: 1px solid darken($code-tag-bg, 15%);
white-space: nowrap; white-space: nowrap;
padding: 2px 5px; padding: 2px 5px;
margin: 0 2px; margin: 0 2px;
...@@ -27,7 +24,7 @@ code { ...@@ -27,7 +24,7 @@ code {
code.code--small { code.code--small {
font-size: $font-size-xs; font-size: $font-size-xs;
padding: 5px; padding: 0.2rem;
margin: 0 2px; margin: 0 2px;
} }
...@@ -41,6 +38,7 @@ pre { ...@@ -41,6 +38,7 @@ pre {
white-space: pre; white-space: pre;
white-space: pre-wrap; white-space: pre-wrap;
background-color: $code-tag-bg; background-color: $code-tag-bg;
padding: 10px;
// Make prettyprint styles more spaced out for readability // Make prettyprint styles more spaced out for readability
&.prettyprint { &.prettyprint {
......
...@@ -92,6 +92,7 @@ define([ ...@@ -92,6 +92,7 @@ define([
var ds = {}; var ds = {};
ds.metricFindQuery = sinon.stub().returns(ctx.$q.when(scenario.queryResult)); ds.metricFindQuery = sinon.stub().returns(ctx.$q.when(scenario.queryResult));
ctx.datasourceSrv.get = sinon.stub().returns(ctx.$q.when(ds)); ctx.datasourceSrv.get = sinon.stub().returns(ctx.$q.when(ds));
ctx.datasourceSrv.getMetricSources = sinon.stub().returns(scenario.metricSources);
ctx.service.updateOptions(scenario.variable); ctx.service.updateOptions(scenario.variable);
ctx.$rootScope.$digest(); ctx.$rootScope.$digest();
...@@ -137,7 +138,6 @@ define([ ...@@ -137,7 +138,6 @@ define([
}); });
}); });
describeUpdateVariable('interval variable with auto', function(scenario) { describeUpdateVariable('interval variable with auto', function(scenario) {
scenario.setup(function() { scenario.setup(function() {
scenario.variable = { type: 'interval', query: '1s,2h,5h,1d', name: 'test', auto: true, auto_count: 10 }; scenario.variable = { type: 'interval', query: '1s,2h,5h,1d', name: 'test', auto: true, auto_count: 10 };
...@@ -284,6 +284,23 @@ define([ ...@@ -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');
});
});
});
}); });
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment