Commit b716a259 by Daniel Lee

mysqlds: add support for key/value template variables

If the template variable query has two columns named __text and __value
then return a list of key values. The value is used when the variable is
interpolated in the query. Allows mapping of texts to ids.
parent c7959ff0
...@@ -106,6 +106,8 @@ This is something we plan to add. ...@@ -106,6 +106,8 @@ This is something we plan to add.
## Templating ## Templating
This feature is currently available in the nightly builds and will be included in the 5.0.0 release.
Instead of hard-coding things like server, application and sensor name in you metric queries you can use variables in their place. Variables are shown as dropdown select boxes at the top of the dashboard. These dropdowns makes it easy to change the data being displayed in your dashboard. Instead of hard-coding things like server, application and sensor name in you metric queries you can use variables in their place. Variables are shown as dropdown select boxes at the top of the dashboard. These dropdowns makes it easy to change the data being displayed in your dashboard.
Checkout the [Templating]({{< relref "reference/templating.md" >}}) documentation for an introduction to the templating feature and the different types of template variables. Checkout the [Templating]({{< relref "reference/templating.md" >}}) documentation for an introduction to the templating feature and the different types of template variables.
...@@ -127,6 +129,12 @@ A query can returns multiple columns and Grafana will automatically create a lis ...@@ -127,6 +129,12 @@ A query can returns multiple columns and Grafana will automatically create a lis
SELECT my_host.hostname, my_other_host.hostname2 FROM my_host JOIN my_other_host ON my_host.city = my_other_host.city SELECT my_host.hostname, my_other_host.hostname2 FROM my_host JOIN my_other_host ON my_host.city = my_other_host.city
``` ```
Another option is a query that can create a key/value variable. The query should return two columns that are named `__text` and `__value`. The `__text` column value should be unique (if it is not unique then the first value is used). The options in the dropdown will have a text and value that allows you to have a friendly name as text and an id as the value. An example query with `hostname` as the text and `id` as the value:
```sql
SELECT hostname AS __text, id AS __value FROM my_host
```
You can also create nested variables. For example if you had another variable named `region`. Then you could have You can also create nested variables. For example if you had another variable named `region`. Then you could have
the hosts variable only show hosts from the current selected region with a query like this (if `region` is a multi-value variable then use the `IN` comparison operator rather than `=` to match against multiple values): the hosts variable only show hosts from the current selected region with a query like this (if `region` is a multi-value variable then use the `IN` comparison operator rather than `=` to match against multiple values):
......
///<reference path="../../../headers/common.d.ts" /> ///<reference path="../../../headers/common.d.ts" />
import _ from 'lodash'; import _ from 'lodash';
import ResponseParser from './response_parser';
export class MysqlDatasource { export class MysqlDatasource {
id: any; id: any;
name: any; name: any;
responseParser: ResponseParser;
/** @ngInject **/ /** @ngInject **/
constructor(instanceSettings, private backendSrv, private $q, private templateSrv) { constructor(instanceSettings, private backendSrv, private $q, private templateSrv) {
this.name = instanceSettings.name; this.name = instanceSettings.name;
this.id = instanceSettings.id; this.id = instanceSettings.id;
this.responseParser = new ResponseParser(this.$q);
} }
interpolateVariable(value) { interpolateVariable(value) {
...@@ -49,7 +52,7 @@ export class MysqlDatasource { ...@@ -49,7 +52,7 @@ export class MysqlDatasource {
to: options.range.to.valueOf().toString(), to: options.range.to.valueOf().toString(),
queries: queries, queries: queries,
} }
}).then(this.processQueryResult.bind(this)); }).then(this.responseParser.processQueryResult);
} }
annotationQuery(options) { annotationQuery(options) {
...@@ -72,46 +75,7 @@ export class MysqlDatasource { ...@@ -72,46 +75,7 @@ export class MysqlDatasource {
to: options.range.to.valueOf().toString(), to: options.range.to.valueOf().toString(),
queries: [query], queries: [query],
} }
}).then(this.transformAnnotationResponse.bind(this, options)); }).then(data => this.responseParser.transformAnnotationResponse(options, data));
}
transformAnnotationResponse(options, data) {
const table = data.data.results[options.annotation.name].tables[0];
let timeColumnIndex = -1;
let titleColumnIndex = -1;
let textColumnIndex = -1;
let tagsColumnIndex = -1;
for (let i = 0; i < table.columns.length; i++) {
if (table.columns[i].text === 'time_sec') {
timeColumnIndex = i;
} else if (table.columns[i].text === 'title') {
titleColumnIndex = i;
} else if (table.columns[i].text === 'text') {
textColumnIndex = i;
} else if (table.columns[i].text === 'tags') {
tagsColumnIndex = i;
}
}
if (timeColumnIndex === -1) {
return this.$q.reject({message: 'Missing mandatory time column (with time_sec column alias) in annotation query.'});
}
const list = [];
for (let i = 0; i < table.rows.length; i++) {
const row = table.rows[i];
list.push({
annotation: options.annotation,
time: Math.floor(row[timeColumnIndex]) * 1000,
title: row[titleColumnIndex],
text: row[textColumnIndex],
tags: row[tagsColumnIndex] ? row[tagsColumnIndex].trim().split(/\s*,\s*/) : []
});
}
return list;
} }
metricFindQuery(query, optionalOptions) { metricFindQuery(query, optionalOptions) {
...@@ -134,26 +98,7 @@ export class MysqlDatasource { ...@@ -134,26 +98,7 @@ export class MysqlDatasource {
queries: [interpolatedQuery], queries: [interpolatedQuery],
} }
}) })
.then(this.transformFindQueryResponse.bind(this, refId)); .then(data => this.responseParser.parseMetricFindQueryResult(refId, data));
}
transformFindQueryResponse(refId, results) {
if (!results || results.data.length === 0 || results.data.results[refId].meta.rowCount === 0) { return []; }
const rows = results.data.results[refId].tables[0].rows;
const res = [];
for (let i = 0; i < rows.length; i++) {
for (let j = 0; j < rows[i].length; j++) {
const value = rows[i][j];
if ( res.indexOf( value ) === -1 ) {
res.push(value);
}
}
}
return _.map(res, value => {
return { text: value};
});
} }
testDatasource() { testDatasource() {
...@@ -183,39 +128,5 @@ export class MysqlDatasource { ...@@ -183,39 +128,5 @@ export class MysqlDatasource {
} }
}); });
} }
processQueryResult(res) {
var data = [];
if (!res.data.results) {
return {data: data};
}
for (let key in res.data.results) {
let queryRes = res.data.results[key];
if (queryRes.series) {
for (let series of queryRes.series) {
data.push({
target: series.name,
datapoints: series.points,
refId: queryRes.refId,
meta: queryRes.meta,
});
}
}
if (queryRes.tables) {
for (let table of queryRes.tables) {
table.type = 'table';
table.refId = queryRes.refId;
table.meta = queryRes.meta;
data.push(table);
}
}
}
return {data: data};
}
} }
///<reference path="../../../headers/common.d.ts" />
import _ from 'lodash';
export default class ResponseParser {
constructor(private $q){}
processQueryResult(res) {
var data = [];
if (!res.data.results) {
return {data: data};
}
for (let key in res.data.results) {
let queryRes = res.data.results[key];
if (queryRes.series) {
for (let series of queryRes.series) {
data.push({
target: series.name,
datapoints: series.points,
refId: queryRes.refId,
meta: queryRes.meta,
});
}
}
if (queryRes.tables) {
for (let table of queryRes.tables) {
table.type = 'table';
table.refId = queryRes.refId;
table.meta = queryRes.meta;
data.push(table);
}
}
}
return {data: data};
}
parseMetricFindQueryResult(refId, results) {
if (!results || results.data.length === 0 || results.data.results[refId].meta.rowCount === 0) { return []; }
const columns = results.data.results[refId].tables[0].columns;
const rows = results.data.results[refId].tables[0].rows;
const textColIndex = this.findColIndex(columns, '__text');
const valueColIndex = this.findColIndex(columns, '__value');
if (columns.length === 2 && textColIndex !== -1 && valueColIndex !== -1){
return this.transformToKeyValueList(rows, textColIndex, valueColIndex);
}
return this.transformToSimpleList(rows);
}
transformToKeyValueList(rows, textColIndex, valueColIndex) {
const res = [];
for (let i = 0; i < rows.length; i++) {
if (!this.containsKey(res, rows[i][textColIndex])) {
res.push({text: rows[i][textColIndex], value: rows[i][valueColIndex]});
}
}
return res;
}
transformToSimpleList(rows) {
const res = [];
for (let i = 0; i < rows.length; i++) {
for (let j = 0; j < rows[i].length; j++) {
const value = rows[i][j];
if ( res.indexOf( value ) === -1 ) {
res.push(value);
}
}
}
return _.map(res, value => {
return { text: value};
});
}
findColIndex(columns, colName) {
for (let i = 0; i < columns.length; i++) {
if (columns[i].text === colName) {
return i;
}
}
return -1;
}
containsKey(res, key) {
for (let i = 0; i < res.length; i++) {
if (res[i].text === key) {
return true;
}
}
return false;
}
transformAnnotationResponse(options, data) {
const table = data.data.results[options.annotation.name].tables[0];
let timeColumnIndex = -1;
let titleColumnIndex = -1;
let textColumnIndex = -1;
let tagsColumnIndex = -1;
for (let i = 0; i < table.columns.length; i++) {
if (table.columns[i].text === 'time_sec') {
timeColumnIndex = i;
} else if (table.columns[i].text === 'title') {
titleColumnIndex = i;
} else if (table.columns[i].text === 'text') {
textColumnIndex = i;
} else if (table.columns[i].text === 'tags') {
tagsColumnIndex = i;
}
}
if (timeColumnIndex === -1) {
return this.$q.reject({message: 'Missing mandatory time column (with time_sec column alias) in annotation query.'});
}
const list = [];
for (let i = 0; i < table.rows.length; i++) {
const row = table.rows[i];
list.push({
annotation: options.annotation,
time: Math.floor(row[timeColumnIndex]) * 1000,
title: row[titleColumnIndex],
text: row[textColumnIndex],
tags: row[tagsColumnIndex] ? row[tagsColumnIndex].trim().split(/\s*,\s*/) : []
});
}
return list;
}
}
...@@ -88,7 +88,7 @@ describe('MySQLDatasource', function() { ...@@ -88,7 +88,7 @@ describe('MySQLDatasource', function() {
refId: 'tempvar', refId: 'tempvar',
tables: [ tables: [
{ {
columns: [{text: 'time_sec'}, {text: 'title'}, {text: 'text'}], columns: [{text: 'title'}, {text: 'text'}],
rows: [ rows: [
['aTitle', 'some text'], ['aTitle', 'some text'],
['aTitle2', 'some text2'], ['aTitle2', 'some text2'],
...@@ -114,4 +114,84 @@ describe('MySQLDatasource', function() { ...@@ -114,4 +114,84 @@ describe('MySQLDatasource', function() {
expect(results[5].text).to.be('some text3'); expect(results[5].text).to.be('some text3');
}); });
}); });
describe('When performing metricFindQuery with key, value columns', function() {
let results;
const query = 'select * from atable';
const response = {
results: {
tempvar: {
meta: {
rowCount: 3
},
refId: 'tempvar',
tables: [
{
columns: [{text: '__value'}, {text: '__text'}],
rows: [
['value1', 'aTitle'],
['value2', 'aTitle2'],
['value3', 'aTitle3']
]
}
]
}
}
};
beforeEach(function() {
ctx.backendSrv.datasourceRequest = function(options) {
return ctx.$q.when({data: response, status: 200});
};
ctx.ds.metricFindQuery(query).then(function(data) { results = data; });
ctx.$rootScope.$apply();
});
it('should return list of as text, value', function() {
expect(results.length).to.be(3);
expect(results[0].text).to.be('aTitle');
expect(results[0].value).to.be('value1');
expect(results[2].text).to.be('aTitle3');
expect(results[2].value).to.be('value3');
});
});
describe('When performing metricFindQuery with key, value columns and with duplicate keys', function() {
let results;
const query = 'select * from atable';
const response = {
results: {
tempvar: {
meta: {
rowCount: 3
},
refId: 'tempvar',
tables: [
{
columns: [{text: '__text'}, {text: '__value'}],
rows: [
['aTitle', 'same'],
['aTitle', 'same'],
['aTitle', 'diff']
]
}
]
}
}
};
beforeEach(function() {
ctx.backendSrv.datasourceRequest = function(options) {
return ctx.$q.when({data: response, status: 200});
};
ctx.ds.metricFindQuery(query).then(function(data) { results = data; });
ctx.$rootScope.$apply();
});
it('should return list of unique keys', function() {
expect(results.length).to.be(1);
expect(results[0].text).to.be('aTitle');
expect(results[0].value).to.be('same');
});
});
}); });
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