mirror of
https://github.com/grafana/grafana.git
synced 2025-02-10 23:55:47 -06:00
mysql: annotation support. Fixes #8382
Simple query editor - a text area with a Show Help section. Validation for empty query and if the time_sec column is missing.
This commit is contained in:
parent
d47078e27f
commit
73cb035231
@ -57,7 +57,12 @@ export class AnnotationsSrv {
|
||||
};
|
||||
|
||||
}).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 {
|
||||
}).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() {
|
||||
return this.backendSrv.datasourceRequest({
|
||||
url: '/api/tsdb/query',
|
||||
|
@ -9,10 +9,33 @@ class MysqlConfigCtrl {
|
||||
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 {
|
||||
MysqlDatasource,
|
||||
MysqlDatasource as Datasource,
|
||||
MysqlQueryCtrl as QueryCtrl,
|
||||
MysqlConfigCtrl as ConfigCtrl,
|
||||
MysqlAnnotationsQueryCtrl as AnnotationsQueryCtrl,
|
||||
};
|
||||
|
||||
|
@ -1,20 +1,34 @@
|
||||
|
||||
<div class="gf-form-group">
|
||||
<h6>Filters</h6>
|
||||
<div class="gf-form-inline">
|
||||
<div class="gf-form">
|
||||
<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 class="gf-form">
|
||||
<span class="gf-form-label width-7">Max limit</span>
|
||||
<div class="gf-form-select-wrapper">
|
||||
<select class="gf-form-input" ng-model="ctrl.annotation.limit" ng-options="f for f in [10,50,100,200,300,500,1000,2000]">
|
||||
</select>
|
||||
</div>
|
||||
<div class="gf-form-inline">
|
||||
<div class="gf-form gf-form--grow">
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="gf-form-inline">
|
||||
<div class="gf-form">
|
||||
<label class="gf-form-label query-keyword" ng-click="ctrl.showHelp = !ctrl.showHelp">
|
||||
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 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) -> UNIX_TIMESTAMP(column) as time_sec
|
||||
- $__timeFilter(column) -> UNIX_TIMESTAMP(time_date_time) > from AND UNIX_TIMESTAMP(time_date_time) < 1492750877
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -17,7 +17,7 @@ export interface QueryMeta {
|
||||
}
|
||||
|
||||
|
||||
var defaulQuery = `SELECT
|
||||
const defaultQuery = `SELECT
|
||||
UNIX_TIMESTAMP(<time_column>) as time_sec,
|
||||
<value column> as value,
|
||||
<series name column> as metric
|
||||
@ -54,7 +54,7 @@ export class MysqlQueryCtrl extends QueryCtrl {
|
||||
this.target.format = 'table';
|
||||
this.target.rawSql = "SELECT 1";
|
||||
} else {
|
||||
this.target.rawSql = defaulQuery;
|
||||
this.target.rawSql = defaultQuery;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,79 @@
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
Loading…
Reference in New Issue
Block a user