Commit 90dbd497 by Torkel Ödegaard

Merge branch 'master' of github.com:grafana/grafana

parents 359421b5 73cb0352
Copyright 2014-2016 Torkel Ödegaard, Raintank Inc. Copyright 2014-2017 Torkel Ödegaard, Raintank Inc.
Licensed under the Apache License, Version 2.0 (the "License"); you Licensed under the Apache License, Version 2.0 (the "License"); you
may not use this file except in compliance with the License. You may may not use this file except in compliance with the License. You may
......
...@@ -57,7 +57,12 @@ export class AnnotationsSrv { ...@@ -57,7 +57,12 @@ export class AnnotationsSrv {
}; };
}).catch(err => { }).catch(err => {
this.$rootScope.appEvent('alert-error', ['Annotations failed', (err.message || err)]); if (!err.message && err.data && err.data.message) {
err.message = err.data.message;
}
this.$rootScope.appEvent('alert-error', ['Annotation Query Failed', (err.message || err)]);
return [];
}); });
} }
......
...@@ -52,6 +52,68 @@ export class MysqlDatasource { ...@@ -52,6 +52,68 @@ export class MysqlDatasource {
}).then(this.processQueryResult.bind(this)); }).then(this.processQueryResult.bind(this));
} }
annotationQuery(options) {
if (!options.annotation.rawQuery) {
return this.$q.reject({message: 'Query missing in annotation definition'});
}
const query = {
refId: options.annotation.name,
datasourceId: this.id,
rawSql: this.templateSrv.replace(options.annotation.rawQuery, options.scopedVars, this.interpolateVariable),
format: 'table',
};
return this.backendSrv.datasourceRequest({
url: '/api/tsdb/query',
method: 'POST',
data: {
from: options.range.from.valueOf().toString(),
to: options.range.to.valueOf().toString(),
queries: [query],
}
}).then(this.transformAnnotationResponse.bind(this, options));
}
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;
}
testDatasource() { testDatasource() {
return this.backendSrv.datasourceRequest({ return this.backendSrv.datasourceRequest({
url: '/api/tsdb/query', url: '/api/tsdb/query',
......
...@@ -9,10 +9,33 @@ class MysqlConfigCtrl { ...@@ -9,10 +9,33 @@ class MysqlConfigCtrl {
static templateUrl = 'partials/config.html'; static templateUrl = 'partials/config.html';
} }
const defaultQuery = `SELECT
UNIX_TIMESTAMP(<time_column>) as time_sec,
<title_column> as title,
<text_column> as text,
<tags_column> as tags
FROM <table name>
WHERE $__timeFilter(time_column)
ORDER BY <time_column> ASC
LIMIT 100
`;
class MysqlAnnotationsQueryCtrl {
static templateUrl = 'partials/annotations.editor.html';
annotation: any;
/** @ngInject **/
constructor() {
this.annotation.rawQuery = this.annotation.rawQuery || defaultQuery;
}
}
export { export {
MysqlDatasource, MysqlDatasource,
MysqlDatasource as Datasource, MysqlDatasource as Datasource,
MysqlQueryCtrl as QueryCtrl, MysqlQueryCtrl as QueryCtrl,
MysqlConfigCtrl as ConfigCtrl, MysqlConfigCtrl as ConfigCtrl,
MysqlAnnotationsQueryCtrl as AnnotationsQueryCtrl,
}; };
<div class="gf-form-group"> <div class="gf-form-group">
<h6>Filters</h6> <div class="gf-form-inline">
<div class="gf-form-inline"> <div class="gf-form gf-form--grow">
<div class="gf-form"> <textarea rows="10" class="gf-form-input" ng-model="ctrl.annotation.rawQuery" spellcheck="false" placeholder="query expression" data-min-length=0 data-items=100 ng-model-onblur ng-change="ctrl.panelCtrl.refresh()"></textarea>
<span class="gf-form-label width-7">Type</span>
<div class="gf-form-select-wrapper">
<select class="gf-form-input" ng-model="ctrl.annotation.type" ng-options="f.value as f.text for f in [{text: 'Alert', value: 'alert'}]">
</select>
</div>
</div> </div>
<div class="gf-form"> </div>
<span class="gf-form-label width-7">Max limit</span>
<div class="gf-form-select-wrapper"> <div class="gf-form-inline">
<select class="gf-form-input" ng-model="ctrl.annotation.limit" ng-options="f for f in [10,50,100,200,300,500,1000,2000]"> <div class="gf-form">
</select> <label class="gf-form-label query-keyword" ng-click="ctrl.showHelp = !ctrl.showHelp">
</div> Show Help
<i class="fa fa-caret-down" ng-show="ctrl.showHelp"></i>
<i class="fa fa-caret-right" ng-hide="ctrl.showHelp"></i>
</label>
</div> </div>
</div>
<div class="gf-form" ng-show="ctrl.showHelp">
<pre class="gf-form-pre alert alert-info"><h6>Annotation Query Format</h6>
An annotation is an event that is overlayed on top of graphs. The query can have up to four columns per row, the time_sec column is mandatory. Annotation rendering is expensive so it is important to limit the number of rows returned.
- column with alias: <b>time_sec</b> for the annotation event. Format is UTC in seconds, use UNIX_TIMESTAMP(column)
- column with alias <b>title</b> for the annotation title
- column with alias: <b>text</b> for the annotation text
- column with alias: <b>tags</b> for annotation tags. This is a comma separated string of tags e.g. 'tag1,tag2'
Macros:
- $__time(column) -&gt; UNIX_TIMESTAMP(column) as time_sec
- $__timeFilter(column) -&gt; UNIX_TIMESTAMP(time_date_time) &gt; from AND UNIX_TIMESTAMP(time_date_time) &lt; 1492750877
</pre>
</div> </div>
</div> </div>
...@@ -17,7 +17,7 @@ export interface QueryMeta { ...@@ -17,7 +17,7 @@ export interface QueryMeta {
} }
var defaulQuery = `SELECT const defaultQuery = `SELECT
UNIX_TIMESTAMP(<time_column>) as time_sec, UNIX_TIMESTAMP(<time_column>) as time_sec,
<value column> as value, <value column> as value,
<series name column> as metric <series name column> as metric
...@@ -54,7 +54,7 @@ export class MysqlQueryCtrl extends QueryCtrl { ...@@ -54,7 +54,7 @@ export class MysqlQueryCtrl extends QueryCtrl {
this.target.format = 'table'; this.target.format = 'table';
this.target.rawSql = "SELECT 1"; this.target.rawSql = "SELECT 1";
} else { } else {
this.target.rawSql = defaulQuery; this.target.rawSql = defaultQuery;
} }
} }
......
import {describe, beforeEach, it, sinon, expect, angularMocks} from 'test/lib/common';
import moment from 'moment';
import helpers from 'test/specs/helpers';
import {MysqlDatasource} from '../datasource';
describe('MySQLDatasource', function() {
var ctx = new helpers.ServiceTestContext();
var instanceSettings = {name: 'mysql'};
beforeEach(angularMocks.module('grafana.core'));
beforeEach(angularMocks.module('grafana.services'));
beforeEach(ctx.providePhase(['backendSrv']));
beforeEach(angularMocks.inject(function($q, $rootScope, $httpBackend, $injector) {
ctx.$q = $q;
ctx.$httpBackend = $httpBackend;
ctx.$rootScope = $rootScope;
ctx.ds = $injector.instantiate(MysqlDatasource, {instanceSettings: instanceSettings});
$httpBackend.when('GET', /\.html$/).respond('');
}));
describe('When performing annotationQuery', function() {
let results;
const annotationName = 'MyAnno';
const options = {
annotation: {
name: annotationName,
rawQuery: 'select time_sec, title, text, tags from table;'
},
range: {
from: moment(1432288354),
to: moment(1432288401)
}
};
const response = {
results: {
MyAnno: {
refId: annotationName,
tables: [
{
columns: [{text: 'time_sec'}, {text: 'title'}, {text: 'text'}, {text: 'tags'}],
rows: [
[1432288355, 'aTitle', 'some text', 'TagA,TagB'],
[1432288390, 'aTitle2', 'some text2', ' TagB , TagC'],
[1432288400, 'aTitle3', 'some text3']
]
}
]
}
}
};
beforeEach(function() {
ctx.backendSrv.datasourceRequest = function(options) {
return ctx.$q.when({data: response, status: 200});
};
ctx.ds.annotationQuery(options).then(function(data) { results = data; });
ctx.$rootScope.$apply();
});
it('should return annotation list', function() {
expect(results.length).to.be(3);
expect(results[0].title).to.be('aTitle');
expect(results[0].text).to.be('some text');
expect(results[0].tags[0]).to.be('TagA');
expect(results[0].tags[1]).to.be('TagB');
expect(results[1].tags[0]).to.be('TagB');
expect(results[1].tags[1]).to.be('TagC');
expect(results[2].tags.length).to.be(0);
});
});
});
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