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 diff --git a/docs/sources/features/datasources/mysql.md b/docs/sources/features/datasources/mysql.md index 2072e6a4f7b..02e821050fa 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,71 @@ 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. +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. + +### 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 +``` + +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): + +```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/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**: 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) 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/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/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() { 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/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); } diff --git a/public/app/plugins/datasource/mysql/datasource.ts b/public/app/plugins/datasource/mysql/datasource.ts index d2603bf8514..dfc990b52e4 100644 --- a/public/app/plugins/datasource/mysql/datasource.ts +++ b/public/app/plugins/datasource/mysql/datasource.ts @@ -1,24 +1,27 @@ /// 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) { if (typeof value === 'string') { - return '\"' + value + '\"'; + return '\'' + value + '\''; } var quotedValues = _.map(value, function(val) { - return '\"' + val + '\"'; + return '\'' + val + '\''; }); return quotedValues.join(','); } @@ -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,30 @@ export class MysqlDatasource { to: options.range.to.valueOf().toString(), 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]; + metricFindQuery(query, optionalOptions) { + let refId = 'tempvar'; + if (optionalOptions && optionalOptions.variable && optionalOptions.variable.name) { + refId = optionalOptions.variable.name; + } - let timeColumnIndex = -1; - let titleColumnIndex = -1; - let textColumnIndex = -1; - let tagsColumnIndex = -1; + const interpolatedQuery = { + refId: refId, + datasourceId: this.id, + rawSql: this.templateSrv.replace(query, {}, this.interpolateVariable), + format: 'table', + }; - 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; + return this.backendSrv.datasourceRequest({ + url: '/api/tsdb/query', + method: 'POST', + data: { + queries: [interpolatedQuery], } - } - - 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.parseMetricFindQueryResult(refId, data)); } testDatasource() { @@ -141,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/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 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 3a82293fb61..11ddaa34086 100644 --- a/public/app/plugins/datasource/mysql/specs/datasource_specs.ts +++ b/public/app/plugins/datasource/mysql/specs/datasource_specs.ts @@ -76,4 +76,122 @@ 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: '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'); + }); + }); + + 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'); + }); + }); }); diff --git a/public/app/plugins/datasource/prometheus/partials/query.editor.html b/public/app/plugins/datasource/prometheus/partials/query.editor.html index fc62089ce85..5140c9e7bbd 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 @@ - +
'; + 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; } }