From 59513ff963b7eb469e94b7bc02e2d41a460433f2 Mon Sep 17 00:00:00 2001 From: Daniel Lee Date: Wed, 9 Aug 2017 14:28:23 +0200 Subject: [PATCH 01/10] tempvar: bug fix for duplicate template var and a fix for when selecting the same value twice in a template variable if it is not multi select. It threw an exception on the second selection --- public/app/core/directives/value_select_dropdown.js | 2 +- public/app/features/templating/editor_ctrl.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/public/app/core/directives/value_select_dropdown.js b/public/app/core/directives/value_select_dropdown.js index 8b1719c669d..a2bae1c34d3 100644 --- a/public/app/core/directives/value_select_dropdown.js +++ b/public/app/core/directives/value_select_dropdown.js @@ -122,7 +122,7 @@ function (angular, _, coreModule) { vm.selectValue = function(option, event, commitChange, excludeOthers) { if (!option) { return; } - option.selected = !option.selected; + option.selected = vm.variable.multi ? !option.selected: true; commitChange = commitChange || false; excludeOthers = excludeOthers || false; diff --git a/public/app/features/templating/editor_ctrl.ts b/public/app/features/templating/editor_ctrl.ts index 9ef5327e7cc..beed1534f5d 100644 --- a/public/app/features/templating/editor_ctrl.ts +++ b/public/app/features/templating/editor_ctrl.ts @@ -106,7 +106,7 @@ export class VariableEditorCtrl { var clone = _.cloneDeep(variable.getSaveModel()); $scope.current = variableSrv.createVariableFromModel(clone); $scope.current.name = 'copy_of_'+variable.name; - $scope.variableSrv.addVariable($scope.current); + variableSrv.addVariable($scope.current); }; $scope.update = function() { From 1105bb371f8c6fec8cf0f241b3df2bee4d15af41 Mon Sep 17 00:00:00 2001 From: Daniel Lee Date: Wed, 9 Aug 2017 18:47:11 +0200 Subject: [PATCH 02/10] mysqlds: adds support for template variables Fixes #8855 --- docs/sources/features/datasources/mysql.md | 68 ++++++++++++++++--- .../app/features/templating/query_variable.ts | 2 +- .../plugins/datasource/mysql/datasource.ts | 46 ++++++++++++- .../mysql/specs/datasource_specs.ts | 38 +++++++++++ 4 files changed, 143 insertions(+), 11 deletions(-) diff --git a/docs/sources/features/datasources/mysql.md b/docs/sources/features/datasources/mysql.md index 2072e6a4f7b..9c208ad3d37 100644 --- a/docs/sources/features/datasources/mysql.md +++ b/docs/sources/features/datasources/mysql.md @@ -29,8 +29,7 @@ data from a MySQL compatible database. The database user you specify when you add the data source should only be granted SELECT permissions on the specified database & tables you want to query. Grafana does not validate that the query is safe. The query could include any SQL statement. For example, statements like `USE otherdb;` and `DROP TABLE user;` would be -executed. To protect against this we **Highly** recommmend you create a specific mysql user with -restricted permissions. +executed. To protect against this we **Highly** recommmend you create a specific mysql user with restricted permissions. Example: @@ -49,11 +48,9 @@ Macro example | Description ------------ | ------------- *$__timeFilter(dateColumn)* | Will be replaced by a time range filter using the specified column name. For example, *dateColumn > FROM_UNIXTIME(1494410783) AND dateColumn < FROM_UNIXTIME(1494497183)* -We plan to add many more macros. If you have suggestions for what macros you would like to see, please -[open an issue](https://github.com/grafana/grafana) in our GitHub repo. +We plan to add many more macros. If you have suggestions for what macros you would like to see, please [open an issue](https://github.com/grafana/grafana) in our GitHub repo. -The query editor has a link named `Generated SQL` that show up after a query as been executed, while in panel edit mode. Click -on it and it will expand and show the raw interpolated SQL string that was executed. +The query editor has a link named `Generated SQL` that show up after a query as been executed, while in panel edit mode. Click on it and it will expand and show the raw interpolated SQL string that was executed. ## Table queries @@ -109,8 +106,63 @@ This is something we plan to add. ## Templating -You can use variables in your queries but there are currently no support for defining `Query` variables -that target a MySQL data source. +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. + +### Query Variable + +If you add a template variable of the type `Query`, you can write a MySQL query that can +return things like measurement names, key names or key values that are shown as a dropdown select box. + +For example, you can have a variable that contains all values for the `hostname` column in a table if you specify a query like this in the templating variable *Query* setting. + +```sql +SELECT hostname FROM my_host +``` + +A query can returns multiple columns and Grafana will automatically create a list from them. For example, the query below will return a list with values from `hostname` and `hostname2`. + +```sql +SELECT my_host.hostname, my_other_host.hostname2 FROM my_host JOIN my_other_host ON my_host.city = my_other_host.city +``` + +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): + +```sql +SELECT hostname FROM my_host WHERE region IN($region) +``` + +### Using Variables in Queries + +Template variables are quoted automatically so if it is a string value do not wrap them in quotes in where clauses. If the variable is a multi-value variable then use the `IN` comparison operator rather than `=` to match against multiple values. + +There are two syntaxes: + +`$` Example with a template variable named `hostname`: + +```sql +SELECT + UNIX_TIMESTAMP(atimestamp) as time_sec, + aint as value, + avarchar as metric +FROM my_table +WHERE $__timeFilter(atimestamp) and hostname in($hostname) +ORDER BY atimestamp ASC +``` + +`[[varname]]` Example with a template variable named `hostname`: + +```sql +SELECT + UNIX_TIMESTAMP(atimestamp) as time_sec, + aint as value, + avarchar as metric +FROM my_table +WHERE $__timeFilter(atimestamp) and hostname in([[hostname]]) +ORDER BY atimestamp ASC +``` ## Alerting diff --git a/public/app/features/templating/query_variable.ts b/public/app/features/templating/query_variable.ts index 04d642547b0..257f6b49dc6 100644 --- a/public/app/features/templating/query_variable.ts +++ b/public/app/features/templating/query_variable.ts @@ -128,7 +128,7 @@ export class QueryVariable implements Variable { } metricFindQuery(datasource, query) { - var options = {range: undefined}; + var options = {range: undefined, variable: this}; if (this.refresh === 2) { options.range = this.timeSrv.timeRange(); diff --git a/public/app/plugins/datasource/mysql/datasource.ts b/public/app/plugins/datasource/mysql/datasource.ts index d2603bf8514..54857789a3c 100644 --- a/public/app/plugins/datasource/mysql/datasource.ts +++ b/public/app/plugins/datasource/mysql/datasource.ts @@ -14,11 +14,11 @@ export class MysqlDatasource { interpolateVariable(value) { if (typeof value === 'string') { - return '\"' + value + '\"'; + return '\'' + value + '\''; } var quotedValues = _.map(value, function(val) { - return '\"' + val + '\"'; + return '\'' + val + '\''; }); return quotedValues.join(','); } @@ -114,6 +114,48 @@ export class MysqlDatasource { return list; } + metricFindQuery(query, optionalOptions) { + let refId = 'tempvar'; + if (optionalOptions && optionalOptions.variable && optionalOptions.variable.name) { + refId = optionalOptions.variable.name; + } + + const interpolatedQuery = { + refId: refId, + datasourceId: this.id, + rawSql: this.templateSrv.replace(query, {}, this.interpolateVariable), + format: 'table', + }; + + return this.backendSrv.datasourceRequest({ + url: '/api/tsdb/query', + method: 'POST', + data: { + queries: [interpolatedQuery], + } + }) + .then(this.transformFindQueryResponse.bind(this, refId)); + } + + 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() { return this.backendSrv.datasourceRequest({ url: '/api/tsdb/query', diff --git a/public/app/plugins/datasource/mysql/specs/datasource_specs.ts b/public/app/plugins/datasource/mysql/specs/datasource_specs.ts index 3a82293fb61..a229e387b70 100644 --- a/public/app/plugins/datasource/mysql/specs/datasource_specs.ts +++ b/public/app/plugins/datasource/mysql/specs/datasource_specs.ts @@ -76,4 +76,42 @@ describe('MySQLDatasource', function() { }); }); + describe('When performing metricFindQuery', function() { + let results; + const query = 'select * from atable'; + const response = { + results: { + tempvar: { + meta: { + rowCount: 3 + }, + refId: 'tempvar', + tables: [ + { + columns: [{text: 'time_sec'}, {text: 'title'}, {text: 'text'}], + rows: [ + ['aTitle', 'some text'], + ['aTitle2', 'some text2'], + ['aTitle3', 'some text3'] + ] + } + ] + } + } + }; + + 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 all column values', function() { + expect(results.length).to.be(6); + expect(results[0].text).to.be('aTitle'); + expect(results[5].text).to.be('some text3'); + }); + }); }); From 43fa852cc1287aea296e568dd4c1e025477fec1c Mon Sep 17 00:00:00 2001 From: Daniel Lee Date: Wed, 9 Aug 2017 19:33:09 +0200 Subject: [PATCH 03/10] mysql: change logging from into to debug for scan --- pkg/tsdb/mysql/mysql.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/tsdb/mysql/mysql.go b/pkg/tsdb/mysql/mysql.go index e38f37b96af..09ded090089 100644 --- a/pkg/tsdb/mysql/mysql.go +++ b/pkg/tsdb/mysql/mysql.go @@ -183,7 +183,7 @@ func (e MysqlExecutor) getTypedRowData(types []*sql.ColumnType, rows *core.Rows) values := make([]interface{}, len(types)) for i, stype := range types { - e.log.Info("type", "type", stype) + e.log.Debug("type", "type", stype) switch stype.DatabaseTypeName() { case mysql.FieldTypeNameTiny: values[i] = new(int8) From d2437d3cf1d562610302f596eece02d2e4e66c6c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Thu, 10 Aug 2017 12:04:46 +0200 Subject: [PATCH 04/10] fix: ad-hoc filters now works with data source variables, fixes #8052 --- .../templating/specs/template_srv_specs.ts | 25 +++++++++++++++ public/app/features/templating/templateSrv.js | 32 +++++++++++-------- .../app/features/templating/variable_srv.ts | 2 -- 3 files changed, 44 insertions(+), 15 deletions(-) diff --git a/public/app/features/templating/specs/template_srv_specs.ts b/public/app/features/templating/specs/template_srv_specs.ts index 673273f240a..3c8b146dc18 100644 --- a/public/app/features/templating/specs/template_srv_specs.ts +++ b/public/app/features/templating/specs/template_srv_specs.ts @@ -51,6 +51,31 @@ describe('templateSrv', function() { }); }); + describe('getAdhocFilters', function() { + beforeEach(function() { + initTemplateSrv([ + {type: 'datasource', name: 'ds', current: {value: 'logstash', text: 'logstash'}}, + {type: 'adhoc', name: 'test', datasource: 'oogle', filters: [1]}, + {type: 'adhoc', name: 'test2', datasource: '$ds', filters: [2]}, + ]); + }); + + it('should return filters if datasourceName match', function() { + var filters = _templateSrv.getAdhocFilters('oogle'); + expect(filters).to.eql([1]); + }); + + it('should return empty array if datasourceName does not match', function() { + var filters = _templateSrv.getAdhocFilters('oogleasdasd'); + expect(filters).to.eql([]); + }); + + it('should return filters when datasourceName match via data source variable', function() { + var filters = _templateSrv.getAdhocFilters('logstash'); + expect(filters).to.eql([2]); + }); + }); + describe('replace can pass multi / all format', function() { beforeEach(function() { initTemplateSrv([{type: 'query', name: 'test', current: {value: ['value1', 'value2'] }}]); diff --git a/public/app/features/templating/templateSrv.js b/public/app/features/templating/templateSrv.js index f47afc81574..31f8bdcbb13 100644 --- a/public/app/features/templating/templateSrv.js +++ b/public/app/features/templating/templateSrv.js @@ -15,7 +15,6 @@ function (angular, _, kbn) { this._index = {}; this._texts = {}; this._grafanaVariables = {}; - this._adhocVariables = {}; // default built ins this._builtIns = {}; @@ -30,24 +29,16 @@ function (angular, _, kbn) { this.updateTemplateData = function() { this._index = {}; this._filters = {}; - this._adhocVariables = {}; for (var i = 0; i < this.variables.length; i++) { var variable = this.variables[i]; - // add adhoc filters to it's own index - if (variable.type === 'adhoc') { - this._adhocVariables[variable.datasource] = variable; - continue; - } - if (!variable.current || !variable.current.isNone && !variable.current.value) { continue; } this._index[variable.name] = variable; } - }; this.variableInitialized = function(variable) { @@ -55,11 +46,26 @@ function (angular, _, kbn) { }; this.getAdhocFilters = function(datasourceName) { - var variable = this._adhocVariables[datasourceName]; - if (variable) { - return variable.filters || []; + var filters = []; + + for (var i = 0; i < this.variables.length; i++) { + var variable = this.variables[i]; + if (variable.type !== 'adhoc') { + continue; + } + + if (variable.datasource === datasourceName) { + filters = filters.concat(variable.filters); + } + + if (variable.datasource.indexOf('$') === 0) { + if (this.replace(variable.datasource) === datasourceName) { + filters = filters.concat(variable.filters); + } + } } - return []; + + return filters; }; function luceneEscape(value) { diff --git a/public/app/features/templating/variable_srv.ts b/public/app/features/templating/variable_srv.ts index c147c5b6685..a0e2dfde23d 100644 --- a/public/app/features/templating/variable_srv.ts +++ b/public/app/features/templating/variable_srv.ts @@ -247,8 +247,6 @@ export class VariableSrv { } filter.operator = options.operator; - - variable.setFilters(filters); this.variableUpdated(variable, true); } From c7959ff06e1151fa1b0b99e916e55f5a21340cbe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Thu, 10 Aug 2017 14:12:31 +0200 Subject: [PATCH 05/10] fix: Using table cell links and ad hoc filters together now works & looks correct, fixes #8052 --- public/app/plugins/panel/table/renderer.ts | 12 ++++++------ public/sass/components/_panel_table.scss | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/public/app/plugins/panel/table/renderer.ts b/public/app/plugins/panel/table/renderer.ts index 02f0ceca72c..cab3f698ae6 100644 --- a/public/app/plugins/panel/table/renderer.ts +++ b/public/app/plugins/panel/table/renderer.ts @@ -157,9 +157,9 @@ export class TableRenderer { // because of the fixed table headers css only solution // there is an issue if header cell is wider the cell // this hack adds header content to cell (not visible) - var widthHack = ''; + var columnHtml = ''; if (addWidthHack) { - widthHack = '
' + this.table.columns[columnIndex].title + '
'; + columnHtml = '
' + this.table.columns[columnIndex].title + '
'; } if (value === undefined) { @@ -173,8 +173,6 @@ export class TableRenderer { cellClasses.push("table-panel-cell-pre"); } - var columnHtml = widthHack + value; - if (column.style && column.style.link) { // Render cell as link var scopedVars = this.renderRowVariables(rowIndex); @@ -185,11 +183,13 @@ export class TableRenderer { var cellTarget = column.style.linkTargetBlank ? '_blank' : ''; cellClasses.push("table-panel-cell-link"); - columnHtml = ` + columnHtml += ` - ${columnHtml} + ${value} `; + } else { + columnHtml += value; } if (column.filterable) { diff --git a/public/sass/components/_panel_table.scss b/public/sass/components/_panel_table.scss index cf81dca4465..77652bc634d 100644 --- a/public/sass/components/_panel_table.scss +++ b/public/sass/components/_panel_table.scss @@ -84,7 +84,7 @@ a { padding: 0.45em 0 0.45em 1.1em; height: 100%; - width: 100%; + display: inline-block; } } From b716a2595ad9c9cffcf6dcfb5c9d960b091bdb02 Mon Sep 17 00:00:00 2001 From: Daniel Lee Date: Thu, 10 Aug 2017 14:39:36 +0200 Subject: [PATCH 06/10] 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. --- docs/sources/features/datasources/mysql.md | 8 + .../plugins/datasource/mysql/datasource.ts | 101 +------------ .../datasource/mysql/response_parser.ts | 143 ++++++++++++++++++ .../mysql/specs/datasource_specs.ts | 82 +++++++++- 4 files changed, 238 insertions(+), 96 deletions(-) create mode 100644 public/app/plugins/datasource/mysql/response_parser.ts diff --git a/docs/sources/features/datasources/mysql.md b/docs/sources/features/datasources/mysql.md index 9c208ad3d37..02e821050fa 100644 --- a/docs/sources/features/datasources/mysql.md +++ b/docs/sources/features/datasources/mysql.md @@ -106,6 +106,8 @@ This is something we plan to add. ## 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. 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 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 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): diff --git a/public/app/plugins/datasource/mysql/datasource.ts b/public/app/plugins/datasource/mysql/datasource.ts index 54857789a3c..dfc990b52e4 100644 --- a/public/app/plugins/datasource/mysql/datasource.ts +++ b/public/app/plugins/datasource/mysql/datasource.ts @@ -1,15 +1,18 @@ /// import _ from 'lodash'; +import ResponseParser from './response_parser'; export class MysqlDatasource { id: any; name: any; + responseParser: ResponseParser; /** @ngInject **/ constructor(instanceSettings, private backendSrv, private $q, private templateSrv) { this.name = instanceSettings.name; this.id = instanceSettings.id; + this.responseParser = new ResponseParser(this.$q); } interpolateVariable(value) { @@ -49,7 +52,7 @@ export class MysqlDatasource { to: options.range.to.valueOf().toString(), queries: queries, } - }).then(this.processQueryResult.bind(this)); + }).then(this.responseParser.processQueryResult); } annotationQuery(options) { @@ -72,46 +75,7 @@ export class MysqlDatasource { 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; + }).then(data => this.responseParser.transformAnnotationResponse(options, data)); } metricFindQuery(query, optionalOptions) { @@ -134,26 +98,7 @@ export class MysqlDatasource { queries: [interpolatedQuery], } }) - .then(this.transformFindQueryResponse.bind(this, refId)); - } - - 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}; - }); + .then(data => this.responseParser.parseMetricFindQueryResult(refId, data)); } testDatasource() { @@ -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}; - } } diff --git a/public/app/plugins/datasource/mysql/response_parser.ts b/public/app/plugins/datasource/mysql/response_parser.ts new file mode 100644 index 00000000000..1b89cc78599 --- /dev/null +++ b/public/app/plugins/datasource/mysql/response_parser.ts @@ -0,0 +1,143 @@ +/// + +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; + } +} diff --git a/public/app/plugins/datasource/mysql/specs/datasource_specs.ts b/public/app/plugins/datasource/mysql/specs/datasource_specs.ts index a229e387b70..11ddaa34086 100644 --- a/public/app/plugins/datasource/mysql/specs/datasource_specs.ts +++ b/public/app/plugins/datasource/mysql/specs/datasource_specs.ts @@ -88,7 +88,7 @@ describe('MySQLDatasource', function() { refId: 'tempvar', tables: [ { - columns: [{text: 'time_sec'}, {text: 'title'}, {text: 'text'}], + columns: [{text: 'title'}, {text: 'text'}], rows: [ ['aTitle', 'some text'], ['aTitle2', 'some text2'], @@ -114,4 +114,84 @@ describe('MySQLDatasource', function() { 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'); + }); + }); }); From c4feef2541621d3397e73599b7217e7f4ef8e52b Mon Sep 17 00:00:00 2001 From: Daniel Lee Date: Fri, 11 Aug 2017 10:24:54 +0200 Subject: [PATCH 07/10] docs: http api /users - corrections + adds missing method --- docs/sources/http_api/user.md | 48 +++++++++++++++++++++++++++-------- 1 file changed, 38 insertions(+), 10 deletions(-) diff --git a/docs/sources/http_api/user.md b/docs/sources/http_api/user.md index fa20fd3e9b3..761ac938cd8 100644 --- a/docs/sources/http_api/user.md +++ b/docs/sources/http_api/user.md @@ -20,9 +20,9 @@ parent = "http_api" GET /api/users HTTP/1.1 Accept: application/json Content-Type: application/json - Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk + Authorization: Basic YWRtaW46YWRtaW4= -Default value for the `perpage` parameter is `1000` and for the `page` parameter is `1`. +Default value for the `perpage` parameter is `1000` and for the `page` parameter is `1`. Requires basic authentication and that the authenticated user is a Grafana Admin. **Example Response**: @@ -55,10 +55,12 @@ Default value for the `perpage` parameter is `1000` and for the `page` parameter GET /api/users/search?perpage=10&page=1&query=mygraf HTTP/1.1 Accept: application/json Content-Type: application/json - Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk + Authorization: Basic YWRtaW46YWRtaW4= Default value for the `perpage` parameter is `1000` and for the `page` parameter is `1`. The `totalCount` field in the response can be used for pagination of the user list E.g. if `totalCount` is equal to 100 users and the `perpage` parameter is set to 10 then there are 10 pages of users. The `query` parameter is optional and it will return results where the query value is contained in one of the `name`, `login` or `email` fields. Query values with spaces need to be url encoded e.g. `query=Jane%20Doe`. +Requires basic authentication and that the authenticated user is a Grafana Admin. + **Example Response**: HTTP/1.1 200 @@ -94,7 +96,9 @@ Default value for the `perpage` parameter is `1000` and for the `page` parameter GET /api/users/1 HTTP/1.1 Accept: application/json Content-Type: application/json - Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk + Authorization: Basic YWRtaW46YWRtaW4= + +Requires basic authentication and that the authenticated user is a Grafana Admin. **Example Response**: @@ -126,7 +130,9 @@ Default value for the `perpage` parameter is `1000` and for the `page` parameter GET /api/users/lookup?loginOrEmail=admin HTTP/1.1 Accept: application/json Content-Type: application/json - Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk + Authorization: Basic YWRtaW46YWRtaW4= + +Requires basic authentication and that the authenticated user is a Grafana Admin. **Example Response**: @@ -152,7 +158,7 @@ Default value for the `perpage` parameter is `1000` and for the `page` parameter PUT /api/users/2 HTTP/1.1 Accept: application/json Content-Type: application/json - Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk + Authorization: Basic YWRtaW46YWRtaW4= { "email":"user@mygraf.com", @@ -161,6 +167,8 @@ Default value for the `perpage` parameter is `1000` and for the `page` parameter "theme":"light" } +Requires basic authentication and that the authenticated user is a Grafana Admin. + **Example Response**: HTTP/1.1 200 @@ -178,7 +186,9 @@ Default value for the `perpage` parameter is `1000` and for the `page` parameter GET /api/users/1/orgs HTTP/1.1 Accept: application/json Content-Type: application/json - Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk + Authorization: Basic YWRtaW46YWRtaW4= + +Requires basic authentication and that the authenticated user is a Grafana Admin. **Example Response**: @@ -246,11 +256,29 @@ Changes the password for the user {"message":"User password changed"} -## Switch user context +## Switch user context for a specified user -`POST /api/user/using/:organisationId` +`POST /api/users/:userId/using/:organizationId` -Switch user context to the given organisation. +Switch user context to the given organization. Requires basic authentication and that the authenticated user is a Grafana Admin. + +**Example Request**: + + POST /api/users/7/using/2 HTTP/1.1 + Authorization: Basic YWRtaW46YWRtaW4= + +**Example Response**: + + HTTP/1.1 200 + Content-Type: application/json + + {"message":"Active organization changed"} + +## Switch user context for signed in user + +`POST /api/user/using/:organizationId` + +Switch user context to the given organization. **Example Request**: From 02248273aece31ec709527dd19f35bf26b2f861f Mon Sep 17 00:00:00 2001 From: Daniel Lee Date: Fri, 11 Aug 2017 10:25:37 +0200 Subject: [PATCH 08/10] mysqlds: remove alpha state from plugin.json --- public/app/plugins/datasource/mysql/plugin.json | 1 - 1 file changed, 1 deletion(-) diff --git a/public/app/plugins/datasource/mysql/plugin.json b/public/app/plugins/datasource/mysql/plugin.json index e26823edbeb..5f35b3f709f 100644 --- a/public/app/plugins/datasource/mysql/plugin.json +++ b/public/app/plugins/datasource/mysql/plugin.json @@ -14,7 +14,6 @@ } }, - "state": "alpha", "alerting": true, "annotations": true, "metrics": true From 167995776fa05396f8717034c67d3c939ba2aa69 Mon Sep 17 00:00:00 2001 From: Daniel Lee Date: Fri, 11 Aug 2017 11:30:30 +0200 Subject: [PATCH 09/10] docs: update to tag alias ref #9024 --- docs/sources/features/datasources/influxdb.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/sources/features/datasources/influxdb.md b/docs/sources/features/datasources/influxdb.md index 3f86b9ee703..8f4f5981124 100644 --- a/docs/sources/features/datasources/influxdb.md +++ b/docs/sources/features/datasources/influxdb.md @@ -88,7 +88,7 @@ You can switch to raw query mode by clicking hamburger icon and then `Switch edi - $m = replaced with measurement name - $measurement = replaced with measurement name - $col = replaced with column name -- $tag_exampletag = replaced with the value of the `exampletag` tag. To use your tag as an alias in the ALIAS BY field then the tag must be used to group by in the query. +- $tag_exampletag = replaced with the value of the `exampletag` tag. The syntax is `$tag_yourTagName` (must start with `$tag_`). To use your tag as an alias in the ALIAS BY field then the tag must be used to group by in the query. - You can also use [[tag_hostname]] pattern replacement syntax. For example, in the ALIAS BY field using this text `Host: [[tag_hostname]]` would substitute in the `hostname` tag value for each legend value and an example legend value would be: `Host: server1`. ### Table query / raw data From d4dd0222fa8f0f2b4d15d2d67ccb6d86a6926998 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Fri, 11 Aug 2017 12:33:40 +0200 Subject: [PATCH 10/10] fix: prometheus does not have two editor modes, fixes #9025 --- public/app/features/dashboard/submenu/submenu.ts | 4 ---- .../plugins/datasource/prometheus/partials/query.editor.html | 2 +- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/public/app/features/dashboard/submenu/submenu.ts b/public/app/features/dashboard/submenu/submenu.ts index 0082f120b5f..69081f01cab 100644 --- a/public/app/features/dashboard/submenu/submenu.ts +++ b/public/app/features/dashboard/submenu/submenu.ts @@ -29,10 +29,6 @@ export class SubmenuCtrl { var search = _.extend(this.$location.search(), {editview: editview}); this.$location.search(search); } - - exitBuildMode() { - this.dashboard.toggleEditMode(); - } } export function submenuDirective() { diff --git a/public/app/plugins/datasource/prometheus/partials/query.editor.html b/public/app/plugins/datasource/prometheus/partials/query.editor.html index 7992d5a8c12..35c6af1280b 100644 --- a/public/app/plugins/datasource/prometheus/partials/query.editor.html +++ b/public/app/plugins/datasource/prometheus/partials/query.editor.html @@ -1,4 +1,4 @@ - +