From 3a3272e225c986cdeb762197a82f84b84b9e769f Mon Sep 17 00:00:00 2001 From: Daniel Lee Date: Tue, 12 Dec 2017 09:35:57 +0100 Subject: [PATCH 001/786] annotations: allows template variables to be used in tag filter When filtering built in annotations by tag, interpolates the tag with template variables. Fixes #9587 --- .../plugins/datasource/grafana/datasource.ts | 7 +- .../grafana/specs/datasource.jest.ts | 65 +++++++++++++++++++ 2 files changed, 71 insertions(+), 1 deletion(-) create mode 100644 public/app/plugins/datasource/grafana/specs/datasource.jest.ts diff --git a/public/app/plugins/datasource/grafana/datasource.ts b/public/app/plugins/datasource/grafana/datasource.ts index 5ca3c433476..9eb9862094a 100644 --- a/public/app/plugins/datasource/grafana/datasource.ts +++ b/public/app/plugins/datasource/grafana/datasource.ts @@ -3,7 +3,7 @@ import _ from 'lodash'; class GrafanaDatasource { /** @ngInject */ - constructor(private backendSrv, private $q) {} + constructor(private backendSrv, private $q, private templateSrv) {} query(options) { return this.backendSrv @@ -58,6 +58,11 @@ class GrafanaDatasource { if (!_.isArray(options.annotation.tags) || options.annotation.tags.length === 0) { return this.$q.when([]); } + const tags = []; + for (let t of params.tags) { + tags.push(this.templateSrv.replace(t)); + } + params.tags = tags; } return this.backendSrv.get('/api/annotations', params); diff --git a/public/app/plugins/datasource/grafana/specs/datasource.jest.ts b/public/app/plugins/datasource/grafana/specs/datasource.jest.ts new file mode 100644 index 00000000000..544b04056ac --- /dev/null +++ b/public/app/plugins/datasource/grafana/specs/datasource.jest.ts @@ -0,0 +1,65 @@ +import {GrafanaDatasource} from "../datasource"; +import q from 'q'; +import moment from 'moment'; + +describe('grafana data source', () => { + describe('when executing an annotations query', () => { + let calledBackendSrvParams; + const backendSrvStub = { + get: (url, options) => { + calledBackendSrvParams = options; + return q.resolve([]); + } + }; + + const templateSrvStub = { + replace: val => val.replace('$var', 'replaced') + }; + + const ds = new GrafanaDatasource(backendSrvStub, q, templateSrvStub); + + describe('with tags that have template variables', () => { + const options = setupAnnotationQueryOptions( + {tags: ['tag1:$var']} + ); + + beforeEach(() => { + return ds.annotationQuery(options); + }); + + it('should interpolate template variables in tags in query options', () => { + expect(calledBackendSrvParams.tags[0]).toBe('tag1:replaced'); + }); + }); + + describe('with type dashboard', () => { + const options = setupAnnotationQueryOptions( + { + type: 'dashboard', + tags: ['tag1'] + }, + {id: 1} + ); + + beforeEach(() => { + return ds.annotationQuery(options); + }); + + it('should remove tags from query options', () => { + expect(calledBackendSrvParams.tags).toBe(undefined); + }); + }); + }); +}); + +function setupAnnotationQueryOptions(annotation, dashboard?) { + return { + annotation: annotation, + dashboard: dashboard, + range: { + from: moment(1432288354), + to: moment(1432288401) + }, + rangeRaw: {from: "now-24h", to: "now"} + }; +} From 33ac22bfdb53eeb7655966a1aed469a8a6f62a7b Mon Sep 17 00:00:00 2001 From: Sven Klemm Date: Sun, 21 Jan 2018 22:08:18 +0100 Subject: [PATCH 002/786] start query builder ui --- .../postgres/partials/query.editor.html | 27 ++++++++++++++----- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/public/app/plugins/datasource/postgres/partials/query.editor.html b/public/app/plugins/datasource/postgres/partials/query.editor.html index 163970a9ad5..635c3e6f222 100644 --- a/public/app/plugins/datasource/postgres/partials/query.editor.html +++ b/public/app/plugins/datasource/postgres/partials/query.editor.html @@ -1,10 +1,23 @@ - -
-
- - -
-
+ + +
+
+
+ + +
+
+
+ +
+
+
+ + + +
+
+
From 17be31e2167ef92af57884a2bb9975a33f7968fb Mon Sep 17 00:00:00 2001 From: Sven Klemm Date: Sun, 28 Jan 2018 22:16:51 +0100 Subject: [PATCH 003/786] call render in query --- .../app/plugins/datasource/postgres/datasource.ts | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/public/app/plugins/datasource/postgres/datasource.ts b/public/app/plugins/datasource/postgres/datasource.ts index 8eee389d1a5..3a4bd27bb4c 100644 --- a/public/app/plugins/datasource/postgres/datasource.ts +++ b/public/app/plugins/datasource/postgres/datasource.ts @@ -1,5 +1,6 @@ import _ from 'lodash'; import ResponseParser from './response_parser'; +import PostgresQuery from 'app/plugins/datasource/postgres/postgres_query'; export class PostgresDatasource { id: any; @@ -33,16 +34,18 @@ export class PostgresDatasource { } query(options) { - var queries = _.filter(options.targets, item => { - return item.hide !== true; - }).map(item => { + var queries = _.filter(options.targets, target => { + return target.hide !== true; + }).map(target => { + var queryModel = new PostgresQuery(target, this.templateSrv, options.scopedVars); + return { - refId: item.refId, + refId: target.refId, intervalMs: options.intervalMs, maxDataPoints: options.maxDataPoints, datasourceId: this.id, - rawSql: this.templateSrv.replace(item.rawSql, options.scopedVars, this.interpolateVariable), - format: item.format, + rawSql: queryModel.render(this.interpolateVariable), + format: target.format, }; }); From a59e052a0f9ac9aab7467d49dd5586de0d08e124 Mon Sep 17 00:00:00 2001 From: Sven Klemm Date: Tue, 30 Jan 2018 14:08:10 +0100 Subject: [PATCH 004/786] more query builder components --- .../postgres/partials/query.editor.html | 94 +++++++++++++------ 1 file changed, 64 insertions(+), 30 deletions(-) diff --git a/public/app/plugins/datasource/postgres/partials/query.editor.html b/public/app/plugins/datasource/postgres/partials/query.editor.html index 635c3e6f222..400855cf82f 100644 --- a/public/app/plugins/datasource/postgres/partials/query.editor.html +++ b/public/app/plugins/datasource/postgres/partials/query.editor.html @@ -1,12 +1,12 @@
-
-
- - -
-
+
+
+ + +
+
@@ -15,42 +15,76 @@ +
+ +
+
+ +
+ +
+ + +
+ +
+ +
+ +
+
+
+
+ +
+
+ + + +
+
+
- -
- -
-
-
+ +
+ +
+
+
-
-
+
+
-
-
-
-
-
+ +
+
+
+ -
-
{{ctrl.lastQueryMeta.sql}}
-
+
+
{{ctrl.lastQueryMeta.sql}}
+
-
-
Time series:
+  
+
Time series:
 - return column named time (UTC in seconds or timestamp)
 - return column(s) with numeric datatype as values
 - (Optional: return column named metric to represent the series name. If no column named metric is found the column name of the value column is used as series name)
@@ -78,13 +112,13 @@ Or build your own conditionals using these macros which just return the values:
 - $__timeTo() ->  to_timestamp(1492750877)
 - $__unixEpochFrom() ->  1492750877
 - $__unixEpochTo() ->  1492750877
-		
-
+
+
- + -
-
{{ctrl.lastQueryError}}
-
+
+
{{ctrl.lastQueryError}}
+
From 438b10bcd68abd115dbe86acad1a70a53a791c79 Mon Sep 17 00:00:00 2001 From: Sven Klemm Date: Tue, 30 Jan 2018 14:10:38 +0100 Subject: [PATCH 005/786] query builder changes --- .../plugins/datasource/postgres/query_ctrl.ts | 179 ++++++++++++++++-- 1 file changed, 168 insertions(+), 11 deletions(-) diff --git a/public/app/plugins/datasource/postgres/query_ctrl.ts b/public/app/plugins/datasource/postgres/query_ctrl.ts index 7afd0cf7253..4e39105370b 100644 --- a/public/app/plugins/datasource/postgres/query_ctrl.ts +++ b/public/app/plugins/datasource/postgres/query_ctrl.ts @@ -1,12 +1,7 @@ import _ from 'lodash'; import { QueryCtrl } from 'app/plugins/sdk'; - -export interface PostgresQuery { - refId: string; - format: string; - alias: string; - rawSql: string; -} +import queryPart from './query_part'; +import PostgresQuery from './postgres_query'; export interface QueryMeta { sql: string; @@ -26,17 +21,21 @@ export class PostgresQueryCtrl extends QueryCtrl { showLastQuerySQL: boolean; formats: any[]; - target: PostgresQuery; + queryModel: PostgresQuery; lastQueryMeta: QueryMeta; lastQueryError: string; showHelp: boolean; + schemaSegment: any; + tableSegment: any; + timeColumnSegment: any; + selectMenu: any; /** @ngInject **/ - constructor($scope, $injector) { + constructor($scope, $injector, private templateSrv, private $q, private uiSegmentSrv) { super($scope, $injector); + this.target = this.target; + this.queryModel = new PostgresQuery(this.target, templateSrv, this.panel.scopedVars); - this.target.format = this.target.format || 'time_series'; - this.target.alias = ''; this.formats = [{ text: 'Time series', value: 'time_series' }, { text: 'Table', value: 'table' }]; if (!this.target.rawSql) { @@ -49,10 +48,104 @@ export class PostgresQueryCtrl extends QueryCtrl { } } + this.schemaSegment= uiSegmentSrv.newSegment(this.target.schema); + + if (!this.target.table) { + this.tableSegment = uiSegmentSrv.newSegment({value: 'select table',fake: true}); + } else { + this.tableSegment= uiSegmentSrv.newSegment(this.target.table); + } + + this.timeColumnSegment = uiSegmentSrv.newSegment(this.target.timeColumn); + + this.buildSelectMenu(); this.panelCtrl.events.on('data-received', this.onDataReceived.bind(this), $scope); this.panelCtrl.events.on('data-error', this.onDataError.bind(this), $scope); } + buildSelectMenu() { + var categories = queryPart.getCategories(); + this.selectMenu = _.reduce( + categories, + function(memo, cat, key) { + var menu = { + text: key, + submenu: cat.map(item => { + return { text: item.type, value: item.type }; + }), + }; + memo.push(menu); + return memo; + }, + [] + ); + } + + toggleEditorMode() { + try { +// this.target.query = this.queryModel.render(false); + } catch (err) { + console.log('query render error'); + } + this.target.rawQuery = !this.target.rawQuery; + } + + getSchemaSegments() { + var schemaQuery = "SELECT schema_name FROM information_schema.schemata WHERE"; + schemaQuery += " schema_name NOT LIKE 'pg_%' AND schema_name <> 'information_schema';"; + return this.datasource + .metricFindQuery(schemaQuery) + .then(this.transformToSegments(true)) + .catch(this.handleQueryError.bind(this)); + } + + getTableSegments() { + var tableQuery = "SELECT table_name FROM information_schema.tables WHERE table_schema = '" + this.target.schema + "';"; + return this.datasource + .metricFindQuery(tableQuery) + .then(this.transformToSegments(true)) + .catch(this.handleQueryError.bind(this)); + } + + getTimeColumnSegments() { + var columnQuery = "SELECT column_name FROM information_schema.columns WHERE "; + columnQuery += " table_schema = '" + this.target.schema + "'"; + columnQuery += " AND table_name = '" + this.target.table + "'"; + columnQuery += " AND data_type IN ('timestamp without time zone','timestamp with time zone','bigint','integer','double precision','real');"; + + return this.datasource + .metricFindQuery(columnQuery) + .then(this.transformToSegments(true)) + .catch(this.handleQueryError.bind(this)); + } + + getColumnSegments() { + var columnQuery = "SELECT column_name FROM information_schema.columns WHERE "; + columnQuery += " table_schema = '" + this.target.schema + "'"; + columnQuery += " AND table_name = '" + this.target.table + "'"; + columnQuery += " AND data_type IN ('bigint','integer','double precision','real');"; + + return this.datasource + .metricFindQuery(columnQuery) + .then(this.transformToSegments(true)) + .catch(this.handleQueryError.bind(this)); + } + + tableChanged() { + this.target.table = this.tableSegment.value; + this.panelCtrl.refresh(); + } + + schemaChanged() { + this.target.schema = this.schemaSegment.value; + this.panelCtrl.refresh(); + } + + timeColumnChanged() { + this.target.time = this.timeColumnSegment.value; + this.panelCtrl.refresh(); + } + onDataReceived(dataList) { this.lastQueryMeta = null; this.lastQueryError = null; @@ -72,4 +165,68 @@ export class PostgresQueryCtrl extends QueryCtrl { } } } + + transformToSegments(addTemplateVars) { + return results => { + var segments = _.map(results, segment => { + return this.uiSegmentSrv.newSegment({ + value: segment.text, + expandable: segment.expandable, + }); + }); + + if (addTemplateVars) { + for (let variable of this.templateSrv.variables) { + segments.unshift( + this.uiSegmentSrv.newSegment({ + type: 'template', + value: '/^$' + variable.name + '$/', + expandable: true, + }) + ); + } + } + + return segments; + }; + } + + addSelectPart(selectParts, cat, subitem) { + this.queryModel.addSelectPart(selectParts, subitem.value); + this.panelCtrl.refresh(); + } + + handleSelectPartEvent(selectParts, part, evt) { + switch (evt.name) { + case 'get-param-options': { + var columnQuery = "SELECT column_name FROM information_schema.columns WHERE "; + columnQuery += " table_schema = '" + this.target.schema + "'"; + columnQuery += " AND table_name = '" + this.target.table + "'"; + columnQuery += " AND data_type IN ('bigint','integer','double precision','real');"; + + return this.datasource + .metricFindQuery(columnQuery) + .then(this.transformToSegments(true)) + .catch(this.handleQueryError.bind(this)); + } + case 'part-param-changed': { + this.panelCtrl.refresh(); + break; + } + case 'action': { + this.queryModel.removeSelectPart(selectParts, part); + this.panelCtrl.refresh(); + break; + } + case 'get-part-actions': { + return this.$q.when([{ text: 'Remove', value: 'remove-part' }]); + } + } + } + + handleQueryError(err) { + this.error = err.message || 'Failed to issue metric query'; + return []; + } + } From 443504517a1bcd58c533feef202530248a6d7050 Mon Sep 17 00:00:00 2001 From: Sven Klemm Date: Tue, 30 Jan 2018 22:13:55 +0100 Subject: [PATCH 006/786] add postgres_query.ts --- .../datasource/postgres/postgres_query.ts | 239 ++++++++++++++++++ 1 file changed, 239 insertions(+) create mode 100644 public/app/plugins/datasource/postgres/postgres_query.ts diff --git a/public/app/plugins/datasource/postgres/postgres_query.ts b/public/app/plugins/datasource/postgres/postgres_query.ts new file mode 100644 index 00000000000..3424ae24c78 --- /dev/null +++ b/public/app/plugins/datasource/postgres/postgres_query.ts @@ -0,0 +1,239 @@ +import _ from 'lodash'; +import queryPart from './query_part'; +import kbn from 'app/core/utils/kbn'; + +export default class PostgresQuery { + target: any; + selectModels: any[]; + queryBuilder: any; + groupByParts: any; + templateSrv: any; + scopedVars: any; + + /** @ngInject */ + constructor(target, templateSrv?, scopedVars?) { + this.target = target; + this.templateSrv = templateSrv; + this.scopedVars = scopedVars; + + target.schema = target.schema || 'public'; + target.format = target.format || 'time_series'; + target.timeColumn = target.timeColumn || 'time'; + target.alias = ''; + + target.orderByTime = target.orderByTime || 'ASC'; +// target.groupBy = target.groupBy || [{ type: 'time', params: ['$__interval'] }, { type: 'fill', params: ['null'] }]; + target.select = target.select || [[{ type: 'field', params: ['value'] }]]; + + this.updateProjection(); + } + + updateProjection() { + this.selectModels = _.map(this.target.select, function(parts: any) { + return _.map(parts, queryPart.create); + }); + this.groupByParts = _.map(this.target.groupBy, queryPart.create); + } + + updatePersistedParts() { + this.target.select = _.map(this.selectModels, function(selectParts) { + return _.map(selectParts, function(part: any) { + return { type: part.def.type, params: part.params }; + }); + }); + } + + hasGroupByTime() { + return _.find(this.target.groupBy, (g: any) => g.type === 'time'); + } + + hasFill() { + return _.find(this.target.groupBy, (g: any) => g.type === 'fill'); + } + + addGroupBy(value) { + var stringParts = value.match(/^(\w+)\((.*)\)$/); + var typePart = stringParts[1]; + var arg = stringParts[2]; + var partModel = queryPart.create({ type: typePart, params: [arg] }); + var partCount = this.target.groupBy.length; + + if (partCount === 0) { + this.target.groupBy.push(partModel.part); + } else if (typePart === 'time') { + this.target.groupBy.splice(0, 0, partModel.part); + } else if (typePart === 'tag') { + if (this.target.groupBy[partCount - 1].type === 'fill') { + this.target.groupBy.splice(partCount - 1, 0, partModel.part); + } else { + this.target.groupBy.push(partModel.part); + } + } else { + this.target.groupBy.push(partModel.part); + } + + this.updateProjection(); + } + + removeGroupByPart(part, index) { + var categories = queryPart.getCategories(); + + if (part.def.type === 'time') { + // remove fill + this.target.groupBy = _.filter(this.target.groupBy, (g: any) => g.type !== 'fill'); + // remove aggregations + this.target.select = _.map(this.target.select, (s: any) => { + return _.filter(s, (part: any) => { + var partModel = queryPart.create(part); + if (partModel.def.category === categories.Aggregations) { + return false; + } + if (partModel.def.category === categories.Selectors) { + return false; + } + return true; + }); + }); + } + + this.target.groupBy.splice(index, 1); + this.updateProjection(); + } + + removeSelect(index: number) { + this.target.select.splice(index, 1); + this.updateProjection(); + } + + removeSelectPart(selectParts, part) { + // if we remove the field remove the whole statement + if (part.def.type === 'field') { + if (this.selectModels.length > 1) { + var modelsIndex = _.indexOf(this.selectModels, selectParts); + this.selectModels.splice(modelsIndex, 1); + } + } else { + var partIndex = _.indexOf(selectParts, part); + selectParts.splice(partIndex, 1); + } + + this.updatePersistedParts(); + } + + addSelectPart(selectParts, type) { + var partModel = queryPart.create({ type: type }); + partModel.def.addStrategy(selectParts, partModel, this); + this.updatePersistedParts(); + } + + private renderTagCondition(tag, index, interpolate) { + var str = ''; + var operator = tag.operator; + var value = tag.value; + if (index > 0) { + str = (tag.condition || 'AND') + ' '; + } + + if (!operator) { + if (/^\/.*\/$/.test(value)) { + operator = '=~'; + } else { + operator = '='; + } + } + + // quote value unless regex + if (operator !== '=~' && operator !== '!~') { + if (interpolate) { + value = this.templateSrv.replace(value, this.scopedVars); + } + if (operator !== '>' && operator !== '<') { + value = "'" + value.replace(/\\/g, '\\\\') + "'"; + } + } else if (interpolate) { + value = this.templateSrv.replace(value, this.scopedVars, 'regex'); + } + + return str + '"' + tag.key + '" ' + operator + ' ' + value; + } + + interpolateQueryStr(value, variable, defaultFormatFn) { + // if no multi or include all do not regexEscape + if (!variable.multi && !variable.includeAll) { + return value; + } + + if (typeof value === 'string') { + return kbn.regexEscape(value); + } + + var escapedValues = _.map(value, kbn.regexEscape); + return '(' + escapedValues.join('|') + ')'; + } + + render(interpolate?) { + var target = this.target; + + if (target.rawQuery) { + if (interpolate) { + return this.templateSrv.replace(target.rawSql, this.scopedVars, this.interpolateQueryStr); + } else { + return target.rawSql; + } + } + + var query = 'SELECT '; + query += target.timeColumn + ' AS time,'; + + var i, y; + for (i = 0; i < this.selectModels.length; i++) { + let parts = this.selectModels[i]; + var selectText = ''; + for (y = 0; y < parts.length; y++) { + let part = parts[y]; + selectText = part.render(selectText); + } + + if (i > 0) { + query += ', '; + } + query += selectText; + } + + query += ' FROM ' + target.schema + '.' + target.table + ' WHERE '; + var conditions = _.map(target.tags, (tag, index) => { + return this.renderTagCondition(tag, index, interpolate); + }); + + if (conditions.length > 0) { + query += '(' + conditions.join(' ') + ') AND '; + } + + query += '$__timeFilter(time)'; + + var groupBySection = ''; + for (i = 0; i < this.groupByParts.length; i++) { + var part = this.groupByParts[i]; + if (i > 0) { + // for some reason fill has no seperator + groupBySection += part.def.type === 'fill' ? ' ' : ', '; + } + groupBySection += part.render(''); + } + + if (groupBySection.length) { + query += ' GROUP BY ' + groupBySection; + } + + query += ' ORDER BY time'; + + return query; + } + + renderAdhocFilters(filters) { + var conditions = _.map(filters, (tag, index) => { + return this.renderTagCondition(tag, index, false); + }); + return conditions.join(' '); + } +} From 571ecdc740b87aad130ce05bd827135fd2e570d4 Mon Sep 17 00:00:00 2001 From: Sven Klemm Date: Tue, 30 Jan 2018 22:57:38 +0100 Subject: [PATCH 007/786] enhance render function --- .../datasource/postgres/postgres_query.ts | 8 +- .../plugins/datasource/postgres/query_part.ts | 380 ++++++++++++++++++ 2 files changed, 386 insertions(+), 2 deletions(-) create mode 100644 public/app/plugins/datasource/postgres/query_part.ts diff --git a/public/app/plugins/datasource/postgres/postgres_query.ts b/public/app/plugins/datasource/postgres/postgres_query.ts index 3424ae24c78..e6d84306a9b 100644 --- a/public/app/plugins/datasource/postgres/postgres_query.ts +++ b/public/app/plugins/datasource/postgres/postgres_query.ts @@ -28,6 +28,10 @@ export default class PostgresQuery { this.updateProjection(); } + quoteIdentifier(field) { + return '"' + field + '"'; + } + updateProjection() { this.selectModels = _.map(this.target.select, function(parts: any) { return _.map(parts, queryPart.create); @@ -183,7 +187,7 @@ export default class PostgresQuery { } var query = 'SELECT '; - query += target.timeColumn + ' AS time,'; + query += this.quoteIdentifier(target.timeColumn) + ' AS time,'; var i, y; for (i = 0; i < this.selectModels.length; i++) { @@ -209,7 +213,7 @@ export default class PostgresQuery { query += '(' + conditions.join(' ') + ') AND '; } - query += '$__timeFilter(time)'; + query += '$__timeFilter(' + this.quoteIdentifier(target.timeColumn) + ')'; var groupBySection = ''; for (i = 0; i < this.groupByParts.length; i++) { diff --git a/public/app/plugins/datasource/postgres/query_part.ts b/public/app/plugins/datasource/postgres/query_part.ts new file mode 100644 index 00000000000..dffea15dbd1 --- /dev/null +++ b/public/app/plugins/datasource/postgres/query_part.ts @@ -0,0 +1,380 @@ +import _ from 'lodash'; +import { QueryPartDef, QueryPart, functionRenderer, suffixRenderer } from 'app/core/components/query_part/query_part'; + +var index = []; +var categories = { + Aggregations: [], + Selectors: [], + Transformations: [], + Predictors: [], + Math: [], + Aliasing: [], + Fields: [], +}; + +function createPart(part): any { + var def = index[part.type]; + if (!def) { + throw { message: 'Could not find query part ' + part.type }; + } + + return new QueryPart(part, def); +} + +function register(options: any) { + index[options.type] = new QueryPartDef(options); + options.category.push(index[options.type]); +} + +var groupByTimeFunctions = []; + +function aliasRenderer(part, innerExpr) { + return innerExpr + ' AS ' + '"' + part.params[0] + '"'; +} + +function fieldRenderer(part, innerExpr) { + return '"' + part.params[0] + '"'; +} + +function replaceAggregationAddStrategy(selectParts, partModel) { + // look for existing aggregation + for (var i = 0; i < selectParts.length; i++) { + var part = selectParts[i]; + if (part.def.category === categories.Aggregations) { + selectParts[i] = partModel; + return; + } + if (part.def.category === categories.Selectors) { + selectParts[i] = partModel; + return; + } + } + + selectParts.splice(1, 0, partModel); +} + +function addTransformationStrategy(selectParts, partModel) { + var i; + // look for index to add transformation + for (i = 0; i < selectParts.length; i++) { + var part = selectParts[i]; + if (part.def.category === categories.Math || part.def.category === categories.Aliasing) { + break; + } + } + + selectParts.splice(i, 0, partModel); +} + +function addMathStrategy(selectParts, partModel) { + var partCount = selectParts.length; + if (partCount > 0) { + // if last is math, replace it + if (selectParts[partCount - 1].def.type === 'math') { + selectParts[partCount - 1] = partModel; + return; + } + // if next to last is math, replace it + if (partCount > 1 && selectParts[partCount - 2].def.type === 'math') { + selectParts[partCount - 2] = partModel; + return; + } else if (selectParts[partCount - 1].def.type === 'alias') { + // if last is alias add it before + selectParts.splice(partCount - 1, 0, partModel); + return; + } + } + selectParts.push(partModel); +} + +function addAliasStrategy(selectParts, partModel) { + var partCount = selectParts.length; + if (partCount > 0) { + // if last is alias, replace it + if (selectParts[partCount - 1].def.type === 'alias') { + selectParts[partCount - 1] = partModel; + return; + } + } + selectParts.push(partModel); +} + +function addFieldStrategy(selectParts, partModel, query) { + // copy all parts + var parts = _.map(selectParts, function(part: any) { + return createPart({ type: part.def.type, params: _.clone(part.params) }); + }); + + query.selectModels.push(parts); +} + +register({ + type: 'field', + addStrategy: addFieldStrategy, + category: categories.Fields, + params: [{ type: 'field', dynamicLookup: true }], + defaultParams: ['value'], + renderer: fieldRenderer, +}); + +// Aggregations +register({ + type: 'avg', + addStrategy: replaceAggregationAddStrategy, + category: categories.Aggregations, + params: [], + defaultParams: [], + renderer: functionRenderer, +}); + +register({ + type: 'count', + addStrategy: replaceAggregationAddStrategy, + category: categories.Aggregations, + params: [], + defaultParams: [], + renderer: functionRenderer, +}); + +register({ + type: 'sum', + addStrategy: replaceAggregationAddStrategy, + category: categories.Aggregations, + params: [], + defaultParams: [], + renderer: functionRenderer, +}); + +// transformations + +register({ + type: 'derivative', + addStrategy: addTransformationStrategy, + category: categories.Transformations, + params: [ + { + name: 'duration', + type: 'interval', + options: ['1s', '10s', '1m', '5m', '10m', '15m', '1h'], + }, + ], + defaultParams: ['10s'], + renderer: functionRenderer, +}); + +register({ + type: 'spread', + addStrategy: addTransformationStrategy, + category: categories.Transformations, + params: [], + defaultParams: [], + renderer: functionRenderer, +}); + +register({ + type: 'non_negative_derivative', + addStrategy: addTransformationStrategy, + category: categories.Transformations, + params: [ + { + name: 'duration', + type: 'interval', + options: ['1s', '10s', '1m', '5m', '10m', '15m', '1h'], + }, + ], + defaultParams: ['10s'], + renderer: functionRenderer, +}); + +register({ + type: 'difference', + addStrategy: addTransformationStrategy, + category: categories.Transformations, + params: [], + defaultParams: [], + renderer: functionRenderer, +}); + +register({ + type: 'non_negative_difference', + addStrategy: addTransformationStrategy, + category: categories.Transformations, + params: [], + defaultParams: [], + renderer: functionRenderer, +}); + +register({ + type: 'moving_average', + addStrategy: addTransformationStrategy, + category: categories.Transformations, + params: [{ name: 'window', type: 'int', options: [5, 10, 20, 30, 40] }], + defaultParams: [10], + renderer: functionRenderer, +}); + +register({ + type: 'cumulative_sum', + addStrategy: addTransformationStrategy, + category: categories.Transformations, + params: [], + defaultParams: [], + renderer: functionRenderer, +}); + +register({ + type: 'stddev', + addStrategy: addTransformationStrategy, + category: categories.Transformations, + params: [], + defaultParams: [], + renderer: functionRenderer, +}); + +register({ + type: 'time', + category: groupByTimeFunctions, + params: [ + { + name: 'interval', + type: 'time', + options: ['$__interval', '1s', '10s', '1m', '5m', '10m', '15m', '1h'], + }, + ], + defaultParams: ['$__interval'], + renderer: functionRenderer, +}); + +register({ + type: 'fill', + category: groupByTimeFunctions, + params: [ + { + name: 'fill', + type: 'string', + options: ['none', 'null', '0', 'previous', 'linear'], + }, + ], + defaultParams: ['null'], + renderer: functionRenderer, +}); + +register({ + type: 'elapsed', + addStrategy: addTransformationStrategy, + category: categories.Transformations, + params: [ + { + name: 'duration', + type: 'interval', + options: ['1s', '10s', '1m', '5m', '10m', '15m', '1h'], + }, + ], + defaultParams: ['10s'], + renderer: functionRenderer, +}); + +// predictions +register({ + type: 'holt_winters', + addStrategy: addTransformationStrategy, + category: categories.Predictors, + params: [ + { name: 'number', type: 'int', options: [5, 10, 20, 30, 40] }, + { name: 'season', type: 'int', options: [0, 1, 2, 5, 10] }, + ], + defaultParams: [10, 2], + renderer: functionRenderer, +}); + +register({ + type: 'holt_winters_with_fit', + addStrategy: addTransformationStrategy, + category: categories.Predictors, + params: [ + { name: 'number', type: 'int', options: [5, 10, 20, 30, 40] }, + { name: 'season', type: 'int', options: [0, 1, 2, 5, 10] }, + ], + defaultParams: [10, 2], + renderer: functionRenderer, +}); + +// Selectors +register({ + type: 'bottom', + addStrategy: replaceAggregationAddStrategy, + category: categories.Selectors, + params: [{ name: 'count', type: 'int' }], + defaultParams: [3], + renderer: functionRenderer, +}); + +register({ + type: 'max', + addStrategy: replaceAggregationAddStrategy, + category: categories.Selectors, + params: [], + defaultParams: [], + renderer: functionRenderer, +}); + +register({ + type: 'min', + addStrategy: replaceAggregationAddStrategy, + category: categories.Selectors, + params: [], + defaultParams: [], + renderer: functionRenderer, +}); + +register({ + type: 'percentile', + addStrategy: replaceAggregationAddStrategy, + category: categories.Selectors, + params: [{ name: 'nth', type: 'int' }], + defaultParams: [95], + renderer: functionRenderer, +}); + +register({ + type: 'top', + addStrategy: replaceAggregationAddStrategy, + category: categories.Selectors, + params: [{ name: 'count', type: 'int' }], + defaultParams: [3], + renderer: functionRenderer, +}); + +register({ + type: 'tag', + category: groupByTimeFunctions, + params: [{ name: 'tag', type: 'string', dynamicLookup: true }], + defaultParams: ['tag'], + renderer: fieldRenderer, +}); + +register({ + type: 'math', + addStrategy: addMathStrategy, + category: categories.Math, + params: [{ name: 'expr', type: 'string' }], + defaultParams: [' / 100'], + renderer: suffixRenderer, +}); + +register({ + type: 'alias', + addStrategy: addAliasStrategy, + category: categories.Aliasing, + params: [{ name: 'name', type: 'string', quote: 'double' }], + defaultParams: ['alias'], + renderMode: 'suffix', + renderer: aliasRenderer, +}); + +export default { + create: createPart, + getCategories: function() { + return categories; + }, +}; From 4dbd83fac18a3cc5e59208d396a32538f2ab1a5e Mon Sep 17 00:00:00 2001 From: Sven Klemm Date: Wed, 31 Jan 2018 15:32:32 +0100 Subject: [PATCH 008/786] add groupby to querybuilder remove unused aggregations --- .../postgres/partials/query.editor.html | 21 +++ .../datasource/postgres/postgres_query.ts | 5 +- .../plugins/datasource/postgres/query_ctrl.ts | 84 ++++++++++ .../plugins/datasource/postgres/query_part.ts | 150 +----------------- 4 files changed, 110 insertions(+), 150 deletions(-) diff --git a/public/app/plugins/datasource/postgres/partials/query.editor.html b/public/app/plugins/datasource/postgres/partials/query.editor.html index 400855cf82f..3e921e1c631 100644 --- a/public/app/plugins/datasource/postgres/partials/query.editor.html +++ b/public/app/plugins/datasource/postgres/partials/query.editor.html @@ -51,6 +51,27 @@ +
+
+ + + + +
+ +
+ +
+ +
+
+
+
+
diff --git a/public/app/plugins/datasource/postgres/postgres_query.ts b/public/app/plugins/datasource/postgres/postgres_query.ts index e6d84306a9b..389ce85aae9 100644 --- a/public/app/plugins/datasource/postgres/postgres_query.ts +++ b/public/app/plugins/datasource/postgres/postgres_query.ts @@ -22,7 +22,7 @@ export default class PostgresQuery { target.alias = ''; target.orderByTime = target.orderByTime || 'ASC'; -// target.groupBy = target.groupBy || [{ type: 'time', params: ['$__interval'] }, { type: 'fill', params: ['null'] }]; + target.groupBy = target.groupBy || [{ type: 'time', params: ['$__interval'] }, { type: 'fill', params: ['null'] }]; target.select = target.select || [[{ type: 'field', params: ['value'] }]]; this.updateProjection(); @@ -92,9 +92,6 @@ export default class PostgresQuery { if (partModel.def.category === categories.Aggregations) { return false; } - if (partModel.def.category === categories.Selectors) { - return false; - } return true; }); }); diff --git a/public/app/plugins/datasource/postgres/query_ctrl.ts b/public/app/plugins/datasource/postgres/query_ctrl.ts index 4e39105370b..020d8007789 100644 --- a/public/app/plugins/datasource/postgres/query_ctrl.ts +++ b/public/app/plugins/datasource/postgres/query_ctrl.ts @@ -29,6 +29,7 @@ export class PostgresQueryCtrl extends QueryCtrl { tableSegment: any; timeColumnSegment: any; selectMenu: any; + groupBySegment: any; /** @ngInject **/ constructor($scope, $injector, private templateSrv, private $q, private uiSegmentSrv) { @@ -59,6 +60,8 @@ export class PostgresQueryCtrl extends QueryCtrl { this.timeColumnSegment = uiSegmentSrv.newSegment(this.target.timeColumn); this.buildSelectMenu(); + this.groupBySegment = this.uiSegmentSrv.newPlusButton(); + this.panelCtrl.events.on('data-received', this.onDataReceived.bind(this), $scope); this.panelCtrl.events.on('data-error', this.onDataError.bind(this), $scope); } @@ -224,6 +227,87 @@ export class PostgresQueryCtrl extends QueryCtrl { } } + handleGroupByPartEvent(part, index, evt) { + switch (evt.name) { + case 'get-param-options': { + var columnQuery = "SELECT column_name FROM information_schema.columns WHERE "; + columnQuery += " table_schema = '" + this.target.schema + "'"; + columnQuery += " AND table_name = '" + this.target.table + "'"; + + return this.datasource + .metricFindQuery(columnQuery) + .then(this.transformToSegments(true)) + .catch(this.handleQueryError.bind(this)); + } + case 'part-param-changed': { + this.panelCtrl.refresh(); + break; + } + case 'action': { + this.queryModel.removeGroupByPart(part, index); + this.panelCtrl.refresh(); + break; + } + case 'get-part-actions': { + return this.$q.when([{ text: 'Remove', value: 'remove-part' }]); + } + } + } + + getGroupByOptions() { + var columnQuery = "SELECT column_name FROM information_schema.columns WHERE "; + columnQuery += " table_schema = '" + this.target.schema + "'"; + columnQuery += " AND table_name = '" + this.target.table + "'"; + + + return this.datasource + .metricFindQuery(columnQuery) + .then(tags => { + var options = []; + if (!this.queryModel.hasFill()) { + options.push(this.uiSegmentSrv.newSegment({ value: 'fill(null)' })); + } + if (!this.target.limit) { + options.push(this.uiSegmentSrv.newSegment({ value: 'LIMIT' })); + } + if (!this.target.slimit) { + options.push(this.uiSegmentSrv.newSegment({ value: 'SLIMIT' })); + } + if (this.target.orderByTime === 'ASC') { + options.push(this.uiSegmentSrv.newSegment({ value: 'ORDER BY time DESC' })); + } + if (!this.queryModel.hasGroupByTime()) { + options.push(this.uiSegmentSrv.newSegment({ value: 'time($interval)' })); + } + for (let tag of tags) { + options.push(this.uiSegmentSrv.newSegment({ value: 'tag(' + tag.text + ')' })); + } + return options; + }) + .catch(this.handleQueryError.bind(this)); + } + + groupByAction() { + switch (this.groupBySegment.value) { + case 'LIMIT': { + this.target.limit = 10; + break; + } + case 'ORDER BY time DESC': { + this.target.orderByTime = 'DESC'; + break; + } + default: { + this.queryModel.addGroupBy(this.groupBySegment.value); + } + } + + var plusButton = this.uiSegmentSrv.newPlusButton(); + this.groupBySegment.value = plusButton.value; + this.groupBySegment.html = plusButton.html; + this.panelCtrl.refresh(); + } + handleQueryError(err) { this.error = err.message || 'Failed to issue metric query'; return []; diff --git a/public/app/plugins/datasource/postgres/query_part.ts b/public/app/plugins/datasource/postgres/query_part.ts index dffea15dbd1..5828515ec06 100644 --- a/public/app/plugins/datasource/postgres/query_part.ts +++ b/public/app/plugins/datasource/postgres/query_part.ts @@ -4,9 +4,6 @@ import { QueryPartDef, QueryPart, functionRenderer, suffixRenderer } from 'app/c var index = []; var categories = { Aggregations: [], - Selectors: [], - Transformations: [], - Predictors: [], Math: [], Aliasing: [], Fields: [], @@ -44,10 +41,6 @@ function replaceAggregationAddStrategy(selectParts, partModel) { selectParts[i] = partModel; return; } - if (part.def.category === categories.Selectors) { - selectParts[i] = partModel; - return; - } } selectParts.splice(1, 0, partModel); @@ -147,34 +140,10 @@ register({ // transformations -register({ - type: 'derivative', - addStrategy: addTransformationStrategy, - category: categories.Transformations, - params: [ - { - name: 'duration', - type: 'interval', - options: ['1s', '10s', '1m', '5m', '10m', '15m', '1h'], - }, - ], - defaultParams: ['10s'], - renderer: functionRenderer, -}); - -register({ - type: 'spread', - addStrategy: addTransformationStrategy, - category: categories.Transformations, - params: [], - defaultParams: [], - renderer: functionRenderer, -}); - register({ type: 'non_negative_derivative', addStrategy: addTransformationStrategy, - category: categories.Transformations, + category: categories.Aggregations, params: [ { name: 'duration', @@ -186,46 +155,10 @@ register({ renderer: functionRenderer, }); -register({ - type: 'difference', - addStrategy: addTransformationStrategy, - category: categories.Transformations, - params: [], - defaultParams: [], - renderer: functionRenderer, -}); - -register({ - type: 'non_negative_difference', - addStrategy: addTransformationStrategy, - category: categories.Transformations, - params: [], - defaultParams: [], - renderer: functionRenderer, -}); - -register({ - type: 'moving_average', - addStrategy: addTransformationStrategy, - category: categories.Transformations, - params: [{ name: 'window', type: 'int', options: [5, 10, 20, 30, 40] }], - defaultParams: [10], - renderer: functionRenderer, -}); - -register({ - type: 'cumulative_sum', - addStrategy: addTransformationStrategy, - category: categories.Transformations, - params: [], - defaultParams: [], - renderer: functionRenderer, -}); - register({ type: 'stddev', addStrategy: addTransformationStrategy, - category: categories.Transformations, + category: categories.Aggregations, params: [], defaultParams: [], renderer: functionRenderer, @@ -259,60 +192,11 @@ register({ renderer: functionRenderer, }); -register({ - type: 'elapsed', - addStrategy: addTransformationStrategy, - category: categories.Transformations, - params: [ - { - name: 'duration', - type: 'interval', - options: ['1s', '10s', '1m', '5m', '10m', '15m', '1h'], - }, - ], - defaultParams: ['10s'], - renderer: functionRenderer, -}); - -// predictions -register({ - type: 'holt_winters', - addStrategy: addTransformationStrategy, - category: categories.Predictors, - params: [ - { name: 'number', type: 'int', options: [5, 10, 20, 30, 40] }, - { name: 'season', type: 'int', options: [0, 1, 2, 5, 10] }, - ], - defaultParams: [10, 2], - renderer: functionRenderer, -}); - -register({ - type: 'holt_winters_with_fit', - addStrategy: addTransformationStrategy, - category: categories.Predictors, - params: [ - { name: 'number', type: 'int', options: [5, 10, 20, 30, 40] }, - { name: 'season', type: 'int', options: [0, 1, 2, 5, 10] }, - ], - defaultParams: [10, 2], - renderer: functionRenderer, -}); - // Selectors -register({ - type: 'bottom', - addStrategy: replaceAggregationAddStrategy, - category: categories.Selectors, - params: [{ name: 'count', type: 'int' }], - defaultParams: [3], - renderer: functionRenderer, -}); - register({ type: 'max', addStrategy: replaceAggregationAddStrategy, - category: categories.Selectors, + category: categories.Aggregations, params: [], defaultParams: [], renderer: functionRenderer, @@ -321,38 +205,12 @@ register({ register({ type: 'min', addStrategy: replaceAggregationAddStrategy, - category: categories.Selectors, + category: categories.Aggregations, params: [], defaultParams: [], renderer: functionRenderer, }); -register({ - type: 'percentile', - addStrategy: replaceAggregationAddStrategy, - category: categories.Selectors, - params: [{ name: 'nth', type: 'int' }], - defaultParams: [95], - renderer: functionRenderer, -}); - -register({ - type: 'top', - addStrategy: replaceAggregationAddStrategy, - category: categories.Selectors, - params: [{ name: 'count', type: 'int' }], - defaultParams: [3], - renderer: functionRenderer, -}); - -register({ - type: 'tag', - category: groupByTimeFunctions, - params: [{ name: 'tag', type: 'string', dynamicLookup: true }], - defaultParams: ['tag'], - renderer: fieldRenderer, -}); - register({ type: 'math', addStrategy: addMathStrategy, From c65a964cdda6e4cabcc570f0199dcd5378331781 Mon Sep 17 00:00:00 2001 From: Sven Klemm Date: Sat, 3 Feb 2018 18:03:00 +0100 Subject: [PATCH 009/786] add metric column selector --- .../postgres/partials/query.editor.html | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/public/app/plugins/datasource/postgres/partials/query.editor.html b/public/app/plugins/datasource/postgres/partials/query.editor.html index 3e921e1c631..e266050dff1 100644 --- a/public/app/plugins/datasource/postgres/partials/query.editor.html +++ b/public/app/plugins/datasource/postgres/partials/query.editor.html @@ -17,6 +17,15 @@
+ +
+ +
+ +
+
+
+
@@ -46,9 +55,15 @@
- + +
+ +
+
+
+
From 382a5254772d29b67658dc1ed8e38a34b7df0a11 Mon Sep 17 00:00:00 2001 From: Sven Klemm Date: Sat, 3 Feb 2018 18:26:57 +0100 Subject: [PATCH 010/786] make metricColumn functional --- .../app/plugins/datasource/postgres/query_ctrl.ts | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/public/app/plugins/datasource/postgres/query_ctrl.ts b/public/app/plugins/datasource/postgres/query_ctrl.ts index 020d8007789..0b2bce7fb05 100644 --- a/public/app/plugins/datasource/postgres/query_ctrl.ts +++ b/public/app/plugins/datasource/postgres/query_ctrl.ts @@ -27,7 +27,9 @@ export class PostgresQueryCtrl extends QueryCtrl { showHelp: boolean; schemaSegment: any; tableSegment: any; + whereSegment: any; timeColumnSegment: any; + metricColumnSegment: any; selectMenu: any; groupBySegment: any; @@ -58,6 +60,7 @@ export class PostgresQueryCtrl extends QueryCtrl { } this.timeColumnSegment = uiSegmentSrv.newSegment(this.target.timeColumn); + this.metricColumnSegment = uiSegmentSrv.newSegment(this.target.metricColumn); this.buildSelectMenu(); this.groupBySegment = this.uiSegmentSrv.newPlusButton(); @@ -122,11 +125,11 @@ export class PostgresQueryCtrl extends QueryCtrl { .catch(this.handleQueryError.bind(this)); } - getColumnSegments() { + getMetricColumnSegments() { var columnQuery = "SELECT column_name FROM information_schema.columns WHERE "; columnQuery += " table_schema = '" + this.target.schema + "'"; columnQuery += " AND table_name = '" + this.target.table + "'"; - columnQuery += " AND data_type IN ('bigint','integer','double precision','real');"; + columnQuery += " AND data_type IN ('text','char','varchar');"; return this.datasource .metricFindQuery(columnQuery) @@ -145,7 +148,12 @@ export class PostgresQueryCtrl extends QueryCtrl { } timeColumnChanged() { - this.target.time = this.timeColumnSegment.value; + this.target.timeColumn = this.timeColumnSegment.value; + this.panelCtrl.refresh(); + } + + metricColumnChanged() { + this.target.metricColumn = this.metricColumnSegment.value; this.panelCtrl.refresh(); } From 3bce45d8a66abf984644f0cad508e73e60b595bc Mon Sep 17 00:00:00 2001 From: Sven Klemm Date: Thu, 8 Feb 2018 10:19:43 +0100 Subject: [PATCH 011/786] add query_builder --- .../datasource/postgres/query_builder.ts | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 public/app/plugins/datasource/postgres/query_builder.ts diff --git a/public/app/plugins/datasource/postgres/query_builder.ts b/public/app/plugins/datasource/postgres/query_builder.ts new file mode 100644 index 00000000000..7a227f33b93 --- /dev/null +++ b/public/app/plugins/datasource/postgres/query_builder.ts @@ -0,0 +1,50 @@ + +export class PostgresQueryBuilder { + constructor(private target, private queryModel) {} + + buildSchemaQuery() { + var query = "SELECT schema_name FROM information_schema.schemata WHERE"; + query += " schema_name NOT LIKE 'pg_%' AND schema_name NOT LIKE '\\_%' AND schema_name <> 'information_schema';"; + + return query; + } + + buildTableQuery() { + var query = "SELECT table_name FROM information_schema.tables WHERE "; + query += "table_schema = " + this.queryModel.quoteLiteral(this.target.schema); + return query; + } + + buildColumnQuery(type?: string) { + var query = "SELECT column_name FROM information_schema.columns WHERE "; + query += "table_schema = " + this.queryModel.quoteLiteral(this.target.schema); + query += " AND table_name = " + this.queryModel.quoteLiteral(this.target.table); + + switch (type) { + case "time": { + query += " AND data_type IN ('timestamp without time zone','timestamp with time zone','bigint','integer','double precision','real')"; + break; + } + case "metric": { + query += " AND data_type IN ('text','char','varchar')"; + break; + } + case "value": { + query += " AND data_type IN ('bigint','integer','double precision','real')"; + break; + } + } + + return query; + } + + buildValueQuery(column: string) { + var query = "SELECT DISTINCT " + this.queryModel.quoteIdentifier(column) + "::text"; + query += " FROM " + this.queryModel.quoteIdentifier(this.target.schema); + query += "." + this.queryModel.quoteIdentifier(this.target.table); + query += " ORDER BY " + this.queryModel.quoteIdentifier(column); + query += " LIMIT 100"; + return query; + } + +} From ef18eb7fcb0449c7bc9709b8837ff8fa202ccc93 Mon Sep 17 00:00:00 2001 From: Sven Klemm Date: Thu, 8 Feb 2018 10:25:32 +0100 Subject: [PATCH 012/786] add where constraint handling --- .../postgres/partials/query.editor.html | 2 +- .../datasource/postgres/postgres_query.ts | 15 +- .../plugins/datasource/postgres/query_ctrl.ts | 192 ++++++++++++++---- 3 files changed, 163 insertions(+), 46 deletions(-) diff --git a/public/app/plugins/datasource/postgres/partials/query.editor.html b/public/app/plugins/datasource/postgres/partials/query.editor.html index e266050dff1..e25a41b11c2 100644 --- a/public/app/plugins/datasource/postgres/partials/query.editor.html +++ b/public/app/plugins/datasource/postgres/partials/query.editor.html @@ -19,7 +19,7 @@
- +
diff --git a/public/app/plugins/datasource/postgres/postgres_query.ts b/public/app/plugins/datasource/postgres/postgres_query.ts index 389ce85aae9..9a5a14c2b6a 100644 --- a/public/app/plugins/datasource/postgres/postgres_query.ts +++ b/public/app/plugins/datasource/postgres/postgres_query.ts @@ -19,17 +19,22 @@ export default class PostgresQuery { target.schema = target.schema || 'public'; target.format = target.format || 'time_series'; target.timeColumn = target.timeColumn || 'time'; - target.alias = ''; + target.metricColumn = target.metricColumn || 'None'; target.orderByTime = target.orderByTime || 'ASC'; - target.groupBy = target.groupBy || [{ type: 'time', params: ['$__interval'] }, { type: 'fill', params: ['null'] }]; + target.groupBy = target.groupBy || []; + target.where = target.where || []; target.select = target.select || [[{ type: 'field', params: ['value'] }]]; this.updateProjection(); } - quoteIdentifier(field) { - return '"' + field + '"'; + quoteIdentifier(value) { + return '"' + value + '"'; + } + + quoteLiteral(value) { + return "'" + value + "'"; } updateProjection() { @@ -202,7 +207,7 @@ export default class PostgresQuery { } query += ' FROM ' + target.schema + '.' + target.table + ' WHERE '; - var conditions = _.map(target.tags, (tag, index) => { + var conditions = _.map(target.where, (tag, index) => { return this.renderTagCondition(tag, index, interpolate); }); diff --git a/public/app/plugins/datasource/postgres/query_ctrl.ts b/public/app/plugins/datasource/postgres/query_ctrl.ts index 0b2bce7fb05..f6f6641eff0 100644 --- a/public/app/plugins/datasource/postgres/query_ctrl.ts +++ b/public/app/plugins/datasource/postgres/query_ctrl.ts @@ -1,4 +1,6 @@ +import angular from 'angular'; import _ from 'lodash'; +import { PostgresQueryBuilder } from './query_builder'; import { QueryCtrl } from 'app/plugins/sdk'; import queryPart from './query_part'; import PostgresQuery from './postgres_query'; @@ -22,22 +24,25 @@ export class PostgresQueryCtrl extends QueryCtrl { showLastQuerySQL: boolean; formats: any[]; queryModel: PostgresQuery; + queryBuilder: PostgresQueryBuilder; lastQueryMeta: QueryMeta; lastQueryError: string; showHelp: boolean; schemaSegment: any; tableSegment: any; - whereSegment: any; + whereSegments: any; timeColumnSegment: any; metricColumnSegment: any; selectMenu: any; groupBySegment: any; + removeWhereFilterSegment: any; /** @ngInject **/ constructor($scope, $injector, private templateSrv, private $q, private uiSegmentSrv) { super($scope, $injector); this.target = this.target; this.queryModel = new PostgresQuery(this.target, templateSrv, this.panel.scopedVars); + this.queryBuilder = new PostgresQueryBuilder(this.target, this.queryModel); this.formats = [{ text: 'Time series', value: 'time_series' }, { text: 'Table', value: 'table' }]; @@ -63,8 +68,32 @@ export class PostgresQueryCtrl extends QueryCtrl { this.metricColumnSegment = uiSegmentSrv.newSegment(this.target.metricColumn); this.buildSelectMenu(); + this.whereSegments = []; + for (let tag of this.target.where) { + if (!tag.operator) { + if (/^\/.*\/$/.test(tag.value)) { + tag.operator = '=~'; + } else { + tag.operator = '='; + } + } + + if (tag.condition) { + this.whereSegments.push(uiSegmentSrv.newCondition(tag.condition)); + } + + this.whereSegments.push(uiSegmentSrv.newKey(tag.key)); + this.whereSegments.push(uiSegmentSrv.newOperator(tag.operator)); + this.whereSegments.push(uiSegmentSrv.newKeyValue(tag.value)); + } + + this.fixWhereSegments(); this.groupBySegment = this.uiSegmentSrv.newPlusButton(); + this.removeWhereFilterSegment = uiSegmentSrv.newSegment({ + fake: true, + value: '-- remove tag filter --', + }); this.panelCtrl.events.on('data-received', this.onDataReceived.bind(this), $scope); this.panelCtrl.events.on('data-error', this.onDataError.bind(this), $scope); } @@ -97,42 +126,29 @@ export class PostgresQueryCtrl extends QueryCtrl { } getSchemaSegments() { - var schemaQuery = "SELECT schema_name FROM information_schema.schemata WHERE"; - schemaQuery += " schema_name NOT LIKE 'pg_%' AND schema_name <> 'information_schema';"; return this.datasource - .metricFindQuery(schemaQuery) + .metricFindQuery(this.queryBuilder.buildSchemaQuery()) .then(this.transformToSegments(true)) .catch(this.handleQueryError.bind(this)); } getTableSegments() { - var tableQuery = "SELECT table_name FROM information_schema.tables WHERE table_schema = '" + this.target.schema + "';"; return this.datasource - .metricFindQuery(tableQuery) + .metricFindQuery(this.queryBuilder.buildTableQuery()) .then(this.transformToSegments(true)) .catch(this.handleQueryError.bind(this)); } getTimeColumnSegments() { - var columnQuery = "SELECT column_name FROM information_schema.columns WHERE "; - columnQuery += " table_schema = '" + this.target.schema + "'"; - columnQuery += " AND table_name = '" + this.target.table + "'"; - columnQuery += " AND data_type IN ('timestamp without time zone','timestamp with time zone','bigint','integer','double precision','real');"; - return this.datasource - .metricFindQuery(columnQuery) + .metricFindQuery(this.queryBuilder.buildColumnQuery("time")) .then(this.transformToSegments(true)) .catch(this.handleQueryError.bind(this)); } getMetricColumnSegments() { - var columnQuery = "SELECT column_name FROM information_schema.columns WHERE "; - columnQuery += " table_schema = '" + this.target.schema + "'"; - columnQuery += " AND table_name = '" + this.target.table + "'"; - columnQuery += " AND data_type IN ('text','char','varchar');"; - return this.datasource - .metricFindQuery(columnQuery) + .metricFindQuery(this.queryBuilder.buildColumnQuery("metric")) .then(this.transformToSegments(true)) .catch(this.handleQueryError.bind(this)); } @@ -210,13 +226,8 @@ export class PostgresQueryCtrl extends QueryCtrl { handleSelectPartEvent(selectParts, part, evt) { switch (evt.name) { case 'get-param-options': { - var columnQuery = "SELECT column_name FROM information_schema.columns WHERE "; - columnQuery += " table_schema = '" + this.target.schema + "'"; - columnQuery += " AND table_name = '" + this.target.table + "'"; - columnQuery += " AND data_type IN ('bigint','integer','double precision','real');"; - return this.datasource - .metricFindQuery(columnQuery) + .metricFindQuery(this.queryBuilder.buildColumnQuery("value")) .then(this.transformToSegments(true)) .catch(this.handleQueryError.bind(this)); } @@ -238,12 +249,8 @@ export class PostgresQueryCtrl extends QueryCtrl { handleGroupByPartEvent(part, index, evt) { switch (evt.name) { case 'get-param-options': { - var columnQuery = "SELECT column_name FROM information_schema.columns WHERE "; - columnQuery += " table_schema = '" + this.target.schema + "'"; - columnQuery += " AND table_name = '" + this.target.table + "'"; - return this.datasource - .metricFindQuery(columnQuery) + .metricFindQuery(this.queryBuilder.buildColumnQuery()) .then(this.transformToSegments(true)) .catch(this.handleQueryError.bind(this)); } @@ -262,14 +269,125 @@ export class PostgresQueryCtrl extends QueryCtrl { } } - getGroupByOptions() { - var columnQuery = "SELECT column_name FROM information_schema.columns WHERE "; - columnQuery += " table_schema = '" + this.target.schema + "'"; - columnQuery += " AND table_name = '" + this.target.table + "'"; + fixWhereSegments() { + var count = this.whereSegments.length; + var lastSegment = this.whereSegments[Math.max(count - 1, 0)]; + if (!lastSegment || lastSegment.type !== 'plus-button') { + this.whereSegments.push(this.uiSegmentSrv.newPlusButton()); + } + } + + getTagsOrValues(segment, index) { + if (segment.type === 'condition') { + return this.$q.when([this.uiSegmentSrv.newSegment('AND'), this.uiSegmentSrv.newSegment('OR')]); + } + if (segment.type === 'operator') { + var nextValue = this.whereSegments[index + 1].value; + if (/^\/.*\/$/.test(nextValue)) { + return this.$q.when(this.uiSegmentSrv.newOperators(['=~', '!~'])); + } else { + return this.$q.when(this.uiSegmentSrv.newOperators(['=', '!=', '<>', '<', '>'])); + } + } + + var query, addTemplateVars; + if (segment.type === 'key' || segment.type === 'plus-button') { + query = this.queryBuilder.buildColumnQuery(); + + addTemplateVars = false; + } else if (segment.type === 'value') { + query = this.queryBuilder.buildValueQuery(this.whereSegments[index -2].value); + addTemplateVars = true; + } return this.datasource - .metricFindQuery(columnQuery) + .metricFindQuery(query) + .then(this.transformToSegments(addTemplateVars)) + .then(results => { + if (segment.type === 'key') { + results.splice(0, 0, angular.copy(this.removeWhereFilterSegment)); + } + return results; + }) + .catch(this.handleQueryError.bind(this)); + } + + getTagValueOperator(tagValue, tagOperator): string { + if (tagOperator !== '=~' && tagOperator !== '!~' && /^\/.*\/$/.test(tagValue)) { + return '=~'; + } else if ((tagOperator === '=~' || tagOperator === '!~') && /^(?!\/.*\/$)/.test(tagValue)) { + return '='; + } + return null; + } + + whereSegmentUpdated(segment, index) { + this.whereSegments[index] = segment; + + // handle remove where condition + if (segment.value === this.removeWhereFilterSegment.value) { + this.whereSegments.splice(index, 3); + if (this.whereSegments.length === 0) { + this.whereSegments.push(this.uiSegmentSrv.newPlusButton()); + } else if (this.whereSegments.length > 2) { + this.whereSegments.splice(Math.max(index - 1, 0), 1); + if (this.whereSegments[this.whereSegments.length - 1].type !== 'plus-button') { + this.whereSegments.push(this.uiSegmentSrv.newPlusButton()); + } + } + } else { + if (segment.type === 'plus-button') { + if (index > 2) { + this.whereSegments.splice(index, 0, this.uiSegmentSrv.newCondition('AND')); + } + this.whereSegments.push(this.uiSegmentSrv.newOperator('=')); + this.whereSegments.push(this.uiSegmentSrv.newFake('select value', 'value', 'query-segment-value')); + segment.type = 'key'; + segment.cssClass = 'query-segment-key'; + } + + if (index + 1 === this.whereSegments.length) { + this.whereSegments.push(this.uiSegmentSrv.newPlusButton()); + } + } + + this.rebuildTargetWhereConditions(); + } + + rebuildTargetWhereConditions() { + var where = []; + var tagIndex = 0; + var tagOperator = ''; + + _.each(this.whereSegments, (segment2, index) => { + if (segment2.type === 'key') { + if (where.length === 0) { + where.push({}); + } + where[tagIndex].key = segment2.value; + } else if (segment2.type === 'value') { + tagOperator = this.getTagValueOperator(segment2.value, where[tagIndex].operator); + if (tagOperator) { + this.whereSegments[index - 1] = this.uiSegmentSrv.newOperator(tagOperator); + where[tagIndex].operator = tagOperator; + } + where[tagIndex].value = segment2.value; + } else if (segment2.type === 'condition') { + where.push({ condition: segment2.value }); + tagIndex += 1; + } else if (segment2.type === 'operator') { + where[tagIndex].operator = segment2.value; + } + }); + + this.target.where = where; + this.panelCtrl.refresh(); + } + + getGroupByOptions() { + return this.datasource + .metricFindQuery(this.queryBuilder.buildColumnQuery()) .then(tags => { var options = []; if (!this.queryModel.hasFill()) { @@ -278,12 +396,6 @@ export class PostgresQueryCtrl extends QueryCtrl { if (!this.target.limit) { options.push(this.uiSegmentSrv.newSegment({ value: 'LIMIT' })); } - if (!this.target.slimit) { - options.push(this.uiSegmentSrv.newSegment({ value: 'SLIMIT' })); - } - if (this.target.orderByTime === 'ASC') { - options.push(this.uiSegmentSrv.newSegment({ value: 'ORDER BY time DESC' })); - } if (!this.queryModel.hasGroupByTime()) { options.push(this.uiSegmentSrv.newSegment({ value: 'time($interval)' })); } From fd518846b1865385eb9775332ffd04fc5388dcc9 Mon Sep 17 00:00:00 2001 From: Sven Klemm Date: Sat, 3 Mar 2018 20:57:00 +0100 Subject: [PATCH 013/786] rename field to column --- .../datasource/postgres/postgres_query.ts | 22 ++++++++----- .../plugins/datasource/postgres/query_ctrl.ts | 7 ++--- .../plugins/datasource/postgres/query_part.ts | 31 +++++++------------ 3 files changed, 27 insertions(+), 33 deletions(-) diff --git a/public/app/plugins/datasource/postgres/postgres_query.ts b/public/app/plugins/datasource/postgres/postgres_query.ts index 9a5a14c2b6a..78e9c9c1a3f 100644 --- a/public/app/plugins/datasource/postgres/postgres_query.ts +++ b/public/app/plugins/datasource/postgres/postgres_query.ts @@ -24,7 +24,7 @@ export default class PostgresQuery { target.orderByTime = target.orderByTime || 'ASC'; target.groupBy = target.groupBy || []; target.where = target.where || []; - target.select = target.select || [[{ type: 'field', params: ['value'] }]]; + target.select = target.select || [[{ type: 'column', params: ['value'] }]]; this.updateProjection(); } @@ -88,8 +88,6 @@ export default class PostgresQuery { var categories = queryPart.getCategories(); if (part.def.type === 'time') { - // remove fill - this.target.groupBy = _.filter(this.target.groupBy, (g: any) => g.type !== 'fill'); // remove aggregations this.target.select = _.map(this.target.select, (s: any) => { return _.filter(s, (part: any) => { @@ -113,7 +111,7 @@ export default class PostgresQuery { removeSelectPart(selectParts, part) { // if we remove the field remove the whole statement - if (part.def.type === 'field') { + if (part.def.type === 'column') { if (this.selectModels.length > 1) { var modelsIndex = _.indexOf(this.selectModels, selectParts); this.selectModels.splice(modelsIndex, 1); @@ -189,7 +187,12 @@ export default class PostgresQuery { } var query = 'SELECT '; - query += this.quoteIdentifier(target.timeColumn) + ' AS time,'; + + if (this.hasGroupByTime()) { + query += '$__timeGroup(' + this.quoteIdentifier(target.timeColumn) + ',1m),'; + } else { + query += this.quoteIdentifier(target.timeColumn) + ' AS time,'; + } var i, y; for (i = 0; i < this.selectModels.length; i++) { @@ -221,10 +224,13 @@ export default class PostgresQuery { for (i = 0; i < this.groupByParts.length; i++) { var part = this.groupByParts[i]; if (i > 0) { - // for some reason fill has no seperator - groupBySection += part.def.type === 'fill' ? ' ' : ', '; + groupBySection += ', '; + } + if (part.def.type === 'time') { + groupBySection += 'time'; + } else { + groupBySection += part.render(''); } - groupBySection += part.render(''); } if (groupBySection.length) { diff --git a/public/app/plugins/datasource/postgres/query_ctrl.ts b/public/app/plugins/datasource/postgres/query_ctrl.ts index f6f6641eff0..0c30e2c51ff 100644 --- a/public/app/plugins/datasource/postgres/query_ctrl.ts +++ b/public/app/plugins/datasource/postgres/query_ctrl.ts @@ -92,7 +92,7 @@ export class PostgresQueryCtrl extends QueryCtrl { this.removeWhereFilterSegment = uiSegmentSrv.newSegment({ fake: true, - value: '-- remove tag filter --', + value: '-- remove filter --', }); this.panelCtrl.events.on('data-received', this.onDataReceived.bind(this), $scope); this.panelCtrl.events.on('data-error', this.onDataError.bind(this), $scope); @@ -390,9 +390,6 @@ export class PostgresQueryCtrl extends QueryCtrl { .metricFindQuery(this.queryBuilder.buildColumnQuery()) .then(tags => { var options = []; - if (!this.queryModel.hasFill()) { - options.push(this.uiSegmentSrv.newSegment({ value: 'fill(null)' })); - } if (!this.target.limit) { options.push(this.uiSegmentSrv.newSegment({ value: 'LIMIT' })); } @@ -400,7 +397,7 @@ export class PostgresQueryCtrl extends QueryCtrl { options.push(this.uiSegmentSrv.newSegment({ value: 'time($interval)' })); } for (let tag of tags) { - options.push(this.uiSegmentSrv.newSegment({ value: 'tag(' + tag.text + ')' })); + options.push(this.uiSegmentSrv.newSegment({ value: tag.text })); } return options; }) diff --git a/public/app/plugins/datasource/postgres/query_part.ts b/public/app/plugins/datasource/postgres/query_part.ts index 5828515ec06..0086b45b848 100644 --- a/public/app/plugins/datasource/postgres/query_part.ts +++ b/public/app/plugins/datasource/postgres/query_part.ts @@ -6,7 +6,7 @@ var categories = { Aggregations: [], Math: [], Aliasing: [], - Fields: [], + Columns: [], }; function createPart(part): any { @@ -29,7 +29,7 @@ function aliasRenderer(part, innerExpr) { return innerExpr + ' AS ' + '"' + part.params[0] + '"'; } -function fieldRenderer(part, innerExpr) { +function columnRenderer(part, innerExpr) { return '"' + part.params[0] + '"'; } @@ -92,7 +92,7 @@ function addAliasStrategy(selectParts, partModel) { selectParts.push(partModel); } -function addFieldStrategy(selectParts, partModel, query) { +function addColumnStrategy(selectParts, partModel, query) { // copy all parts var parts = _.map(selectParts, function(part: any) { return createPart({ type: part.def.type, params: _.clone(part.params) }); @@ -102,12 +102,12 @@ function addFieldStrategy(selectParts, partModel, query) { } register({ - type: 'field', - addStrategy: addFieldStrategy, - category: categories.Fields, - params: [{ type: 'field', dynamicLookup: true }], + type: 'column', + addStrategy: addColumnStrategy, + category: categories.Columns, + params: [{ type: 'column', dynamicLookup: true }], defaultParams: ['value'], - renderer: fieldRenderer, + renderer: columnRenderer, }); // Aggregations @@ -170,25 +170,16 @@ register({ params: [ { name: 'interval', - type: 'time', + type: 'interval', options: ['$__interval', '1s', '10s', '1m', '5m', '10m', '15m', '1h'], }, - ], - defaultParams: ['$__interval'], - renderer: functionRenderer, -}); - -register({ - type: 'fill', - category: groupByTimeFunctions, - params: [ { name: 'fill', type: 'string', - options: ['none', 'null', '0', 'previous', 'linear'], + options: ['none', 'null', '0'], }, ], - defaultParams: ['null'], + defaultParams: ['$__interval','none'], renderer: functionRenderer, }); From 7104e6f9f8dccd5bbba53519daaa40a7cce6a329 Mon Sep 17 00:00:00 2001 From: Sven Klemm Date: Sat, 3 Mar 2018 22:11:51 +0100 Subject: [PATCH 014/786] fix variable interpolation --- public/app/plugins/datasource/postgres/postgres_query.ts | 3 +++ public/app/plugins/datasource/postgres/query_ctrl.ts | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/public/app/plugins/datasource/postgres/postgres_query.ts b/public/app/plugins/datasource/postgres/postgres_query.ts index 78e9c9c1a3f..2f8b3911ebc 100644 --- a/public/app/plugins/datasource/postgres/postgres_query.ts +++ b/public/app/plugins/datasource/postgres/postgres_query.ts @@ -239,6 +239,9 @@ export default class PostgresQuery { query += ' ORDER BY time'; + if (interpolate) { + query = this.templateSrv.replace(query, this.scopedVars, this.interpolateQueryStr); + } return query; } diff --git a/public/app/plugins/datasource/postgres/query_ctrl.ts b/public/app/plugins/datasource/postgres/query_ctrl.ts index 0c30e2c51ff..bde4010e226 100644 --- a/public/app/plugins/datasource/postgres/query_ctrl.ts +++ b/public/app/plugins/datasource/postgres/query_ctrl.ts @@ -207,7 +207,7 @@ export class PostgresQueryCtrl extends QueryCtrl { segments.unshift( this.uiSegmentSrv.newSegment({ type: 'template', - value: '/^$' + variable.name + '$/', + value: '$' + variable.name, expandable: true, }) ); From e8c6341fed3811b3d6339dc5e2fc2a8815caa285 Mon Sep 17 00:00:00 2001 From: Sven Klemm Date: Sun, 4 Mar 2018 10:18:11 +0100 Subject: [PATCH 015/786] clean up aggregation functions --- .../plugins/datasource/postgres/query_part.ts | 87 ++++++------------- 1 file changed, 28 insertions(+), 59 deletions(-) diff --git a/public/app/plugins/datasource/postgres/query_part.ts b/public/app/plugins/datasource/postgres/query_part.ts index 0086b45b848..600723be00d 100644 --- a/public/app/plugins/datasource/postgres/query_part.ts +++ b/public/app/plugins/datasource/postgres/query_part.ts @@ -46,19 +46,6 @@ function replaceAggregationAddStrategy(selectParts, partModel) { selectParts.splice(1, 0, partModel); } -function addTransformationStrategy(selectParts, partModel) { - var i; - // look for index to add transformation - for (i = 0; i < selectParts.length; i++) { - var part = selectParts[i]; - if (part.def.category === categories.Math || part.def.category === categories.Aliasing) { - break; - } - } - - selectParts.splice(i, 0, partModel); -} - function addMathStrategy(selectParts, partModel) { var partCount = selectParts.length; if (partCount > 0) { @@ -138,54 +125,8 @@ register({ renderer: functionRenderer, }); -// transformations - -register({ - type: 'non_negative_derivative', - addStrategy: addTransformationStrategy, - category: categories.Aggregations, - params: [ - { - name: 'duration', - type: 'interval', - options: ['1s', '10s', '1m', '5m', '10m', '15m', '1h'], - }, - ], - defaultParams: ['10s'], - renderer: functionRenderer, -}); - register({ type: 'stddev', - addStrategy: addTransformationStrategy, - category: categories.Aggregations, - params: [], - defaultParams: [], - renderer: functionRenderer, -}); - -register({ - type: 'time', - category: groupByTimeFunctions, - params: [ - { - name: 'interval', - type: 'interval', - options: ['$__interval', '1s', '10s', '1m', '5m', '10m', '15m', '1h'], - }, - { - name: 'fill', - type: 'string', - options: ['none', 'null', '0'], - }, - ], - defaultParams: ['$__interval','none'], - renderer: functionRenderer, -}); - -// Selectors -register({ - type: 'max', addStrategy: replaceAggregationAddStrategy, category: categories.Aggregations, params: [], @@ -202,6 +143,15 @@ register({ renderer: functionRenderer, }); +register({ + type: 'max', + addStrategy: replaceAggregationAddStrategy, + category: categories.Aggregations, + params: [], + defaultParams: [], + renderer: functionRenderer, +}); + register({ type: 'math', addStrategy: addMathStrategy, @@ -221,6 +171,25 @@ register({ renderer: aliasRenderer, }); +register({ + type: 'time', + category: groupByTimeFunctions, + params: [ + { + name: 'interval', + type: 'interval', + options: ['$__interval', '1s', '10s', '1m', '5m', '10m', '15m', '1h'], + }, + { + name: 'fill', + type: 'string', + options: ['none', 'NULL', '0'], + }, + ], + defaultParams: ['$__interval','none'], + renderer: functionRenderer, +}); + export default { create: createPart, getCategories: function() { From 26e09b598c809e93a120dfbc30214af8ff9ac690 Mon Sep 17 00:00:00 2001 From: Sven Klemm Date: Sun, 4 Mar 2018 19:35:43 +0100 Subject: [PATCH 016/786] fix group by column --- .../datasource/postgres/postgres_query.ts | 24 ++++++++++++------- .../plugins/datasource/postgres/query_ctrl.ts | 4 ++-- 2 files changed, 18 insertions(+), 10 deletions(-) diff --git a/public/app/plugins/datasource/postgres/postgres_query.ts b/public/app/plugins/datasource/postgres/postgres_query.ts index 2f8b3911ebc..a236fa91b18 100644 --- a/public/app/plugins/datasource/postgres/postgres_query.ts +++ b/public/app/plugins/datasource/postgres/postgres_query.ts @@ -61,17 +61,17 @@ export default class PostgresQuery { } addGroupBy(value) { - var stringParts = value.match(/^(\w+)\((.*)\)$/); + var stringParts = value.match(/^(\w+)(\((.*)\))?$/); var typePart = stringParts[1]; - var arg = stringParts[2]; - var partModel = queryPart.create({ type: typePart, params: [arg] }); + var args = stringParts[3].split(","); + var partModel = queryPart.create({ type: typePart, params: args }); var partCount = this.target.groupBy.length; if (partCount === 0) { this.target.groupBy.push(partModel.part); } else if (typePart === 'time') { this.target.groupBy.splice(0, 0, partModel.part); - } else if (typePart === 'tag') { + } else if (typePart === 'column') { if (this.target.groupBy[partCount - 1].type === 'fill') { this.target.groupBy.splice(partCount - 1, 0, partModel.part); } else { @@ -188,8 +188,16 @@ export default class PostgresQuery { var query = 'SELECT '; - if (this.hasGroupByTime()) { - query += '$__timeGroup(' + this.quoteIdentifier(target.timeColumn) + ',1m),'; + var timeGroup = this.hasGroupByTime(); + + if (timeGroup) { + var args; + if (timeGroup.params.length > 1 && timeGroup.params[1] !== "none") { + args = timeGroup.params.join(","); + } else { + args = timeGroup.params[0]; + } + query += '$__timeGroup(' + this.quoteIdentifier(target.timeColumn) + ',' + args + '),'; } else { query += this.quoteIdentifier(target.timeColumn) + ' AS time,'; } @@ -227,7 +235,7 @@ export default class PostgresQuery { groupBySection += ', '; } if (part.def.type === 'time') { - groupBySection += 'time'; + groupBySection += '1'; } else { groupBySection += part.render(''); } @@ -237,7 +245,7 @@ export default class PostgresQuery { query += ' GROUP BY ' + groupBySection; } - query += ' ORDER BY time'; + query += ' ORDER BY 1'; if (interpolate) { query = this.templateSrv.replace(query, this.scopedVars, this.interpolateQueryStr); diff --git a/public/app/plugins/datasource/postgres/query_ctrl.ts b/public/app/plugins/datasource/postgres/query_ctrl.ts index bde4010e226..b0d5d7ab9ca 100644 --- a/public/app/plugins/datasource/postgres/query_ctrl.ts +++ b/public/app/plugins/datasource/postgres/query_ctrl.ts @@ -394,10 +394,10 @@ export class PostgresQueryCtrl extends QueryCtrl { options.push(this.uiSegmentSrv.newSegment({ value: 'LIMIT' })); } if (!this.queryModel.hasGroupByTime()) { - options.push(this.uiSegmentSrv.newSegment({ value: 'time($interval)' })); + options.push(this.uiSegmentSrv.newSegment({ type: 'time', value: 'time(1m,none)' })); } for (let tag of tags) { - options.push(this.uiSegmentSrv.newSegment({ value: tag.text })); + options.push(this.uiSegmentSrv.newSegment({ type: 'column', value: 'column(' + tag.text + ')' })); } return options; }) From bf4a30d30f93aa7ac1cf01c319666242022272fe Mon Sep 17 00:00:00 2001 From: Sven Klemm Date: Sun, 4 Mar 2018 19:46:11 +0100 Subject: [PATCH 017/786] set rawSQL when rendering query builder query --- public/app/plugins/datasource/postgres/postgres_query.ts | 1 + public/app/plugins/datasource/postgres/query_ctrl.ts | 5 ----- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/public/app/plugins/datasource/postgres/postgres_query.ts b/public/app/plugins/datasource/postgres/postgres_query.ts index a236fa91b18..a708f384972 100644 --- a/public/app/plugins/datasource/postgres/postgres_query.ts +++ b/public/app/plugins/datasource/postgres/postgres_query.ts @@ -247,6 +247,7 @@ export default class PostgresQuery { query += ' ORDER BY 1'; + this.target.rawSql = query; if (interpolate) { query = this.templateSrv.replace(query, this.scopedVars, this.interpolateQueryStr); } diff --git a/public/app/plugins/datasource/postgres/query_ctrl.ts b/public/app/plugins/datasource/postgres/query_ctrl.ts index b0d5d7ab9ca..4e36e8699ae 100644 --- a/public/app/plugins/datasource/postgres/query_ctrl.ts +++ b/public/app/plugins/datasource/postgres/query_ctrl.ts @@ -117,11 +117,6 @@ export class PostgresQueryCtrl extends QueryCtrl { } toggleEditorMode() { - try { -// this.target.query = this.queryModel.render(false); - } catch (err) { - console.log('query render error'); - } this.target.rawQuery = !this.target.rawQuery; } From 83200c289a454e978ac4c0ade432ac7ccb31dbc4 Mon Sep 17 00:00:00 2001 From: Sven Klemm Date: Sun, 4 Mar 2018 23:32:23 +0100 Subject: [PATCH 018/786] use metricColumn in query builder --- public/app/plugins/datasource/postgres/postgres_query.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/public/app/plugins/datasource/postgres/postgres_query.ts b/public/app/plugins/datasource/postgres/postgres_query.ts index a708f384972..4541ebae7de 100644 --- a/public/app/plugins/datasource/postgres/postgres_query.ts +++ b/public/app/plugins/datasource/postgres/postgres_query.ts @@ -217,6 +217,10 @@ export default class PostgresQuery { query += selectText; } + if (this.target.metricColumn !== 'None') { + query += "," + this.quoteIdentifier(this.target.metricColumn) + " AS metric"; + } + query += ' FROM ' + target.schema + '.' + target.table + ' WHERE '; var conditions = _.map(target.where, (tag, index) => { return this.renderTagCondition(tag, index, interpolate); From 340f679d0f7c531eaa332c036a3141eb40119bff Mon Sep 17 00:00:00 2001 From: Sven Klemm Date: Fri, 9 Mar 2018 18:09:59 +0100 Subject: [PATCH 019/786] quote schema and table --- public/app/plugins/datasource/postgres/postgres_query.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/app/plugins/datasource/postgres/postgres_query.ts b/public/app/plugins/datasource/postgres/postgres_query.ts index 4541ebae7de..bd66ab5eca9 100644 --- a/public/app/plugins/datasource/postgres/postgres_query.ts +++ b/public/app/plugins/datasource/postgres/postgres_query.ts @@ -221,7 +221,7 @@ export default class PostgresQuery { query += "," + this.quoteIdentifier(this.target.metricColumn) + " AS metric"; } - query += ' FROM ' + target.schema + '.' + target.table + ' WHERE '; + query += ' FROM ' + this.quoteIdentifier(target.schema) + '.' + this.quoteIdentifier(target.table) + ' WHERE '; var conditions = _.map(target.where, (tag, index) => { return this.renderTagCondition(tag, index, interpolate); }); From 1d8540ac69d37b84689eaf0016b8d155d8e899a3 Mon Sep 17 00:00:00 2001 From: Sven Klemm Date: Fri, 9 Mar 2018 18:18:12 +0100 Subject: [PATCH 020/786] properly quote where constraint parts --- .../datasource/postgres/postgres_query.ts | 28 ++++--------------- 1 file changed, 6 insertions(+), 22 deletions(-) diff --git a/public/app/plugins/datasource/postgres/postgres_query.ts b/public/app/plugins/datasource/postgres/postgres_query.ts index bd66ab5eca9..9e682d2f186 100644 --- a/public/app/plugins/datasource/postgres/postgres_query.ts +++ b/public/app/plugins/datasource/postgres/postgres_query.ts @@ -130,7 +130,7 @@ export default class PostgresQuery { this.updatePersistedParts(); } - private renderTagCondition(tag, index, interpolate) { + private renderWhereConstraint(tag, index, interpolate) { var str = ''; var operator = tag.operator; var value = tag.value; @@ -138,27 +138,11 @@ export default class PostgresQuery { str = (tag.condition || 'AND') + ' '; } - if (!operator) { - if (/^\/.*\/$/.test(value)) { - operator = '=~'; - } else { - operator = '='; - } + if (interpolate) { + value = this.templateSrv.replace(value, this.scopedVars); } - // quote value unless regex - if (operator !== '=~' && operator !== '!~') { - if (interpolate) { - value = this.templateSrv.replace(value, this.scopedVars); - } - if (operator !== '>' && operator !== '<') { - value = "'" + value.replace(/\\/g, '\\\\') + "'"; - } - } else if (interpolate) { - value = this.templateSrv.replace(value, this.scopedVars, 'regex'); - } - - return str + '"' + tag.key + '" ' + operator + ' ' + value; + return str + this.quoteIdentifier(tag.key) + ' ' + operator + ' ' + this.quoteLiteral(value); } interpolateQueryStr(value, variable, defaultFormatFn) { @@ -223,7 +207,7 @@ export default class PostgresQuery { query += ' FROM ' + this.quoteIdentifier(target.schema) + '.' + this.quoteIdentifier(target.table) + ' WHERE '; var conditions = _.map(target.where, (tag, index) => { - return this.renderTagCondition(tag, index, interpolate); + return this.renderWhereConstraint(tag, index, interpolate); }); if (conditions.length > 0) { @@ -260,7 +244,7 @@ export default class PostgresQuery { renderAdhocFilters(filters) { var conditions = _.map(filters, (tag, index) => { - return this.renderTagCondition(tag, index, false); + return this.renderWhereConstraint(tag, index, false); }); return conditions.join(' '); } From cb5278d413e85ec7fbd21490ca4edc467f691518 Mon Sep 17 00:00:00 2001 From: Sven Klemm Date: Sat, 10 Mar 2018 20:18:03 +0100 Subject: [PATCH 021/786] handle variables in where constraints --- .../datasource/postgres/postgres_query.ts | 16 ++++++---------- .../plugins/datasource/postgres/query_ctrl.ts | 8 ++------ 2 files changed, 8 insertions(+), 16 deletions(-) diff --git a/public/app/plugins/datasource/postgres/postgres_query.ts b/public/app/plugins/datasource/postgres/postgres_query.ts index 9e682d2f186..9c48a94862f 100644 --- a/public/app/plugins/datasource/postgres/postgres_query.ts +++ b/public/app/plugins/datasource/postgres/postgres_query.ts @@ -56,10 +56,6 @@ export default class PostgresQuery { return _.find(this.target.groupBy, (g: any) => g.type === 'time'); } - hasFill() { - return _.find(this.target.groupBy, (g: any) => g.type === 'fill'); - } - addGroupBy(value) { var stringParts = value.match(/^(\w+)(\((.*)\))?$/); var typePart = stringParts[1]; @@ -130,19 +126,19 @@ export default class PostgresQuery { this.updatePersistedParts(); } - private renderWhereConstraint(tag, index, interpolate) { + private renderWhereConstraint(constraint, index, interpolate) { var str = ''; - var operator = tag.operator; - var value = tag.value; + var operator = constraint.operator; + var value = constraint.value; if (index > 0) { - str = (tag.condition || 'AND') + ' '; + str = (constraint.condition || 'AND') + ' '; } if (interpolate) { value = this.templateSrv.replace(value, this.scopedVars); } - return str + this.quoteIdentifier(tag.key) + ' ' + operator + ' ' + this.quoteLiteral(value); + return str + this.quoteIdentifier(constraint.key) + ' ' + operator + ' ' + this.quoteLiteral(value); } interpolateQueryStr(value, variable, defaultFormatFn) { @@ -207,7 +203,7 @@ export default class PostgresQuery { query += ' FROM ' + this.quoteIdentifier(target.schema) + '.' + this.quoteIdentifier(target.table) + ' WHERE '; var conditions = _.map(target.where, (tag, index) => { - return this.renderWhereConstraint(tag, index, interpolate); + return this.renderWhereConstraint(tag, index, false); }); if (conditions.length > 0) { diff --git a/public/app/plugins/datasource/postgres/query_ctrl.ts b/public/app/plugins/datasource/postgres/query_ctrl.ts index 4e36e8699ae..da8c28eab9c 100644 --- a/public/app/plugins/datasource/postgres/query_ctrl.ts +++ b/public/app/plugins/datasource/postgres/query_ctrl.ts @@ -353,7 +353,6 @@ export class PostgresQueryCtrl extends QueryCtrl { rebuildTargetWhereConditions() { var where = []; var tagIndex = 0; - var tagOperator = ''; _.each(this.whereSegments, (segment2, index) => { if (segment2.type === 'key') { @@ -362,11 +361,8 @@ export class PostgresQueryCtrl extends QueryCtrl { } where[tagIndex].key = segment2.value; } else if (segment2.type === 'value') { - tagOperator = this.getTagValueOperator(segment2.value, where[tagIndex].operator); - if (tagOperator) { - this.whereSegments[index - 1] = this.uiSegmentSrv.newOperator(tagOperator); - where[tagIndex].operator = tagOperator; - } + where[tagIndex].value = segment2.value; + } else if (segment2.type === 'template') { where[tagIndex].value = segment2.value; } else if (segment2.type === 'condition') { where.push({ condition: segment2.value }); From 0b358ff5f30930401a3cfa9ca35699d2903786dc Mon Sep 17 00:00:00 2001 From: Sven Klemm Date: Sat, 10 Mar 2018 22:39:42 +0100 Subject: [PATCH 022/786] remove limit --- .../postgres/partials/query.editor.html | 2 +- .../plugins/datasource/postgres/query_ctrl.ts | 31 ++----------------- 2 files changed, 4 insertions(+), 29 deletions(-) diff --git a/public/app/plugins/datasource/postgres/partials/query.editor.html b/public/app/plugins/datasource/postgres/partials/query.editor.html index e25a41b11c2..0706cf3a6cc 100644 --- a/public/app/plugins/datasource/postgres/partials/query.editor.html +++ b/public/app/plugins/datasource/postgres/partials/query.editor.html @@ -19,7 +19,7 @@
- +
diff --git a/public/app/plugins/datasource/postgres/query_ctrl.ts b/public/app/plugins/datasource/postgres/query_ctrl.ts index da8c28eab9c..101d52ee096 100644 --- a/public/app/plugins/datasource/postgres/query_ctrl.ts +++ b/public/app/plugins/datasource/postgres/query_ctrl.ts @@ -273,17 +273,12 @@ export class PostgresQueryCtrl extends QueryCtrl { } } - getTagsOrValues(segment, index) { + getWhereSegments(segment, index) { if (segment.type === 'condition') { return this.$q.when([this.uiSegmentSrv.newSegment('AND'), this.uiSegmentSrv.newSegment('OR')]); } if (segment.type === 'operator') { - var nextValue = this.whereSegments[index + 1].value; - if (/^\/.*\/$/.test(nextValue)) { - return this.$q.when(this.uiSegmentSrv.newOperators(['=~', '!~'])); - } else { - return this.$q.when(this.uiSegmentSrv.newOperators(['=', '!=', '<>', '<', '>'])); - } + return this.$q.when(this.uiSegmentSrv.newOperators(['=', '!=', '<>', '<', '>'])); } var query, addTemplateVars; @@ -308,15 +303,6 @@ export class PostgresQueryCtrl extends QueryCtrl { .catch(this.handleQueryError.bind(this)); } - getTagValueOperator(tagValue, tagOperator): string { - if (tagOperator !== '=~' && tagOperator !== '!~' && /^\/.*\/$/.test(tagValue)) { - return '=~'; - } else if ((tagOperator === '=~' || tagOperator === '!~') && /^(?!\/.*\/$)/.test(tagValue)) { - return '='; - } - return null; - } - whereSegmentUpdated(segment, index) { this.whereSegments[index] = segment; @@ -381,14 +367,11 @@ export class PostgresQueryCtrl extends QueryCtrl { .metricFindQuery(this.queryBuilder.buildColumnQuery()) .then(tags => { var options = []; - if (!this.target.limit) { - options.push(this.uiSegmentSrv.newSegment({ value: 'LIMIT' })); - } if (!this.queryModel.hasGroupByTime()) { options.push(this.uiSegmentSrv.newSegment({ type: 'time', value: 'time(1m,none)' })); } for (let tag of tags) { - options.push(this.uiSegmentSrv.newSegment({ type: 'column', value: 'column(' + tag.text + ')' })); + options.push(this.uiSegmentSrv.newSegment({ type: 'column', value: tag.text })); } return options; }) @@ -397,14 +380,6 @@ export class PostgresQueryCtrl extends QueryCtrl { groupByAction() { switch (this.groupBySegment.value) { - case 'LIMIT': { - this.target.limit = 10; - break; - } - case 'ORDER BY time DESC': { - this.target.orderByTime = 'DESC'; - break; - } default: { this.queryModel.addGroupBy(this.groupBySegment.value); } From e780b1bce5140a6c74fcc4782090e15035c5268a Mon Sep 17 00:00:00 2001 From: Sven Klemm Date: Sun, 11 Mar 2018 12:06:54 +0100 Subject: [PATCH 023/786] cleanup where segment handling --- .../plugins/datasource/postgres/query_ctrl.ts | 34 +++++++------------ 1 file changed, 13 insertions(+), 21 deletions(-) diff --git a/public/app/plugins/datasource/postgres/query_ctrl.ts b/public/app/plugins/datasource/postgres/query_ctrl.ts index 101d52ee096..afde342474b 100644 --- a/public/app/plugins/datasource/postgres/query_ctrl.ts +++ b/public/app/plugins/datasource/postgres/query_ctrl.ts @@ -68,26 +68,7 @@ export class PostgresQueryCtrl extends QueryCtrl { this.metricColumnSegment = uiSegmentSrv.newSegment(this.target.metricColumn); this.buildSelectMenu(); - this.whereSegments = []; - for (let tag of this.target.where) { - if (!tag.operator) { - if (/^\/.*\/$/.test(tag.value)) { - tag.operator = '=~'; - } else { - tag.operator = '='; - } - } - - if (tag.condition) { - this.whereSegments.push(uiSegmentSrv.newCondition(tag.condition)); - } - - this.whereSegments.push(uiSegmentSrv.newKey(tag.key)); - this.whereSegments.push(uiSegmentSrv.newOperator(tag.operator)); - this.whereSegments.push(uiSegmentSrv.newKeyValue(tag.value)); - } - - this.fixWhereSegments(); + this.buildWhereSegments(); this.groupBySegment = this.uiSegmentSrv.newPlusButton(); this.removeWhereFilterSegment = uiSegmentSrv.newSegment({ @@ -264,7 +245,18 @@ export class PostgresQueryCtrl extends QueryCtrl { } } - fixWhereSegments() { + buildWhereSegments() { + this.whereSegments = []; + for (let constraint of this.target.where) { + + if (constraint.condition) { + this.whereSegments.push(this.uiSegmentSrv.newCondition(constraint.condition)); + } + this.whereSegments.push(this.uiSegmentSrv.newKey(constraint.key)); + this.whereSegments.push(this.uiSegmentSrv.newOperator(constraint.operator)); + this.whereSegments.push(this.uiSegmentSrv.newKeyValue(constraint.value)); + } + var count = this.whereSegments.length; var lastSegment = this.whereSegments[Math.max(count - 1, 0)]; From cdb4e2ba0b44741ed9efc09fbf060a7dd5d0c6ac Mon Sep 17 00:00:00 2001 From: Sven Klemm Date: Tue, 13 Mar 2018 21:31:07 +0100 Subject: [PATCH 024/786] remove unused setting --- public/app/plugins/datasource/postgres/postgres_query.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/public/app/plugins/datasource/postgres/postgres_query.ts b/public/app/plugins/datasource/postgres/postgres_query.ts index 9c48a94862f..f9b87e89541 100644 --- a/public/app/plugins/datasource/postgres/postgres_query.ts +++ b/public/app/plugins/datasource/postgres/postgres_query.ts @@ -21,7 +21,6 @@ export default class PostgresQuery { target.timeColumn = target.timeColumn || 'time'; target.metricColumn = target.metricColumn || 'None'; - target.orderByTime = target.orderByTime || 'ASC'; target.groupBy = target.groupBy || []; target.where = target.where || []; target.select = target.select || [[{ type: 'column', params: ['value'] }]]; From b7c7030a4681e3c5c8d3e565588a02e9e8a2e9db Mon Sep 17 00:00:00 2001 From: Sven Klemm Date: Tue, 13 Mar 2018 23:06:39 +0100 Subject: [PATCH 025/786] add regex operators --- .../datasource/postgres/query_builder.ts | 8 ++++++++ .../plugins/datasource/postgres/query_ctrl.ts | 18 ++++++++++++++++-- 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/public/app/plugins/datasource/postgres/query_builder.ts b/public/app/plugins/datasource/postgres/query_builder.ts index 7a227f33b93..23691830b17 100644 --- a/public/app/plugins/datasource/postgres/query_builder.ts +++ b/public/app/plugins/datasource/postgres/query_builder.ts @@ -47,4 +47,12 @@ export class PostgresQueryBuilder { return query; } + buildDatatypeQuery(column: string) { + var query = "SELECT data_type FROM information_schema.columns WHERE "; + query += " table_schema = " + this.queryModel.quoteLiteral(this.target.schema); + query += " AND table_name = " + this.queryModel.quoteLiteral(this.target.table); + query += " AND column_name = " + this.queryModel.quoteLiteral(column); + return query; + } + } diff --git a/public/app/plugins/datasource/postgres/query_ctrl.ts b/public/app/plugins/datasource/postgres/query_ctrl.ts index afde342474b..b84b0ce0750 100644 --- a/public/app/plugins/datasource/postgres/query_ctrl.ts +++ b/public/app/plugins/datasource/postgres/query_ctrl.ts @@ -266,14 +266,28 @@ export class PostgresQueryCtrl extends QueryCtrl { } getWhereSegments(segment, index) { + var query, addTemplateVars; + if (segment.type === 'condition') { return this.$q.when([this.uiSegmentSrv.newSegment('AND'), this.uiSegmentSrv.newSegment('OR')]); } if (segment.type === 'operator') { - return this.$q.when(this.uiSegmentSrv.newOperators(['=', '!=', '<>', '<', '>'])); + var columnName = this.whereSegments[index - 1].value; + query = this.queryBuilder.buildDatatypeQuery(columnName); + return this.datasource.metricFindQuery(query) + .then(results => { + var datatype = results[0].text; + switch (datatype) { + case "text": + case "character varying": + return this.$q.when(this.uiSegmentSrv.newOperators(['=', '!=', '~', '~*','!~','!~*','IN'])); + default: + return this.$q.when(this.uiSegmentSrv.newOperators(['=', '!=', '<', '<=', '>', '>='])); + } + }) + .catch(this.handleQueryError.bind(this)); } - var query, addTemplateVars; if (segment.type === 'key' || segment.type === 'plus-button') { query = this.queryBuilder.buildColumnQuery(); From 958646d976ad64a3b32707c2a7b686a906cd9fc1 Mon Sep 17 00:00:00 2001 From: Sven Klemm Date: Tue, 13 Mar 2018 23:15:25 +0100 Subject: [PATCH 026/786] dont quote where constraints --- public/app/plugins/datasource/postgres/postgres_query.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/app/plugins/datasource/postgres/postgres_query.ts b/public/app/plugins/datasource/postgres/postgres_query.ts index f9b87e89541..48f7efae760 100644 --- a/public/app/plugins/datasource/postgres/postgres_query.ts +++ b/public/app/plugins/datasource/postgres/postgres_query.ts @@ -137,7 +137,7 @@ export default class PostgresQuery { value = this.templateSrv.replace(value, this.scopedVars); } - return str + this.quoteIdentifier(constraint.key) + ' ' + operator + ' ' + this.quoteLiteral(value); + return str + constraint.key + ' ' + operator + ' ' + value; } interpolateQueryStr(value, variable, defaultFormatFn) { From 5e9a66de5f36c8e29ebeb5370f503529ac2dd7c7 Mon Sep 17 00:00:00 2001 From: Sven Klemm Date: Tue, 13 Mar 2018 23:19:56 +0100 Subject: [PATCH 027/786] put values for IN in parens --- public/app/plugins/datasource/postgres/postgres_query.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/public/app/plugins/datasource/postgres/postgres_query.ts b/public/app/plugins/datasource/postgres/postgres_query.ts index 48f7efae760..e06cb3f2126 100644 --- a/public/app/plugins/datasource/postgres/postgres_query.ts +++ b/public/app/plugins/datasource/postgres/postgres_query.ts @@ -137,7 +137,11 @@ export default class PostgresQuery { value = this.templateSrv.replace(value, this.scopedVars); } - return str + constraint.key + ' ' + operator + ' ' + value; + if (operator === "IN") { + return str + constraint.key + ' ' + operator + ' (' + value + ')'; + } else { + return str + constraint.key + ' ' + operator + ' ' + value; + } } interpolateQueryStr(value, variable, defaultFormatFn) { From e6501f0f0ecec19ccafd9191bcfcdef4a1e19c6f Mon Sep 17 00:00:00 2001 From: Sven Klemm Date: Tue, 13 Mar 2018 23:24:26 +0100 Subject: [PATCH 028/786] revert special handling for IN --- public/app/plugins/datasource/postgres/postgres_query.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/public/app/plugins/datasource/postgres/postgres_query.ts b/public/app/plugins/datasource/postgres/postgres_query.ts index e06cb3f2126..48f7efae760 100644 --- a/public/app/plugins/datasource/postgres/postgres_query.ts +++ b/public/app/plugins/datasource/postgres/postgres_query.ts @@ -137,11 +137,7 @@ export default class PostgresQuery { value = this.templateSrv.replace(value, this.scopedVars); } - if (operator === "IN") { - return str + constraint.key + ' ' + operator + ' (' + value + ')'; - } else { - return str + constraint.key + ' ' + operator + ' ' + value; - } + return str + constraint.key + ' ' + operator + ' ' + value; } interpolateQueryStr(value, variable, defaultFormatFn) { From 6793fa5e549dfe31c04dd3dacfa476a9091d3f3a Mon Sep 17 00:00:00 2001 From: Sven Klemm Date: Tue, 13 Mar 2018 23:33:40 +0100 Subject: [PATCH 029/786] join multivalue variables with , --- public/app/plugins/datasource/postgres/postgres_query.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/app/plugins/datasource/postgres/postgres_query.ts b/public/app/plugins/datasource/postgres/postgres_query.ts index 48f7efae760..3a11c344f3e 100644 --- a/public/app/plugins/datasource/postgres/postgres_query.ts +++ b/public/app/plugins/datasource/postgres/postgres_query.ts @@ -151,7 +151,7 @@ export default class PostgresQuery { } var escapedValues = _.map(value, kbn.regexEscape); - return '(' + escapedValues.join('|') + ')'; + return '(' + escapedValues.join(',') + ')'; } render(interpolate?) { From 64fa1ce8a0a3bfe6b7bb4e3c7cbc4227d0c42af4 Mon Sep 17 00:00:00 2001 From: Sven Klemm Date: Wed, 14 Mar 2018 18:09:47 +0100 Subject: [PATCH 030/786] properly handle IN queries --- .../app/plugins/datasource/postgres/postgres_query.ts | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/public/app/plugins/datasource/postgres/postgres_query.ts b/public/app/plugins/datasource/postgres/postgres_query.ts index 3a11c344f3e..8f9f1261340 100644 --- a/public/app/plugins/datasource/postgres/postgres_query.ts +++ b/public/app/plugins/datasource/postgres/postgres_query.ts @@ -1,6 +1,5 @@ import _ from 'lodash'; import queryPart from './query_part'; -import kbn from 'app/core/utils/kbn'; export default class PostgresQuery { target: any; @@ -26,14 +25,16 @@ export default class PostgresQuery { target.select = target.select || [[{ type: 'column', params: ['value'] }]]; this.updateProjection(); + // give interpolateQueryStr access to this + this.interpolateQueryStr = this.interpolateQueryStr.bind(this); } quoteIdentifier(value) { - return '"' + value + '"'; + return '"' + value.replace('"','""') + '"'; } quoteLiteral(value) { - return "'" + value + "'"; + return "'" + value.replace("'","''") + "'"; } updateProjection() { @@ -147,10 +148,10 @@ export default class PostgresQuery { } if (typeof value === 'string') { - return kbn.regexEscape(value); + return this.quoteLiteral(value); } - var escapedValues = _.map(value, kbn.regexEscape); + var escapedValues = _.map(value, this.quoteLiteral); return '(' + escapedValues.join(',') + ')'; } From 0c3afd0e9c098c83f04f44f048f72468496aec5c Mon Sep 17 00:00:00 2001 From: Sven Klemm Date: Wed, 14 Mar 2018 22:59:48 +0100 Subject: [PATCH 031/786] add buildAggregateQuery --- public/app/plugins/datasource/postgres/query_builder.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/public/app/plugins/datasource/postgres/query_builder.ts b/public/app/plugins/datasource/postgres/query_builder.ts index 23691830b17..275d15492fe 100644 --- a/public/app/plugins/datasource/postgres/query_builder.ts +++ b/public/app/plugins/datasource/postgres/query_builder.ts @@ -55,4 +55,12 @@ export class PostgresQueryBuilder { return query; } + buildAggregateQuery() { + var query = "SELECT DISTINCT proname FROM pg_aggregate "; + query += "INNER JOIN pg_proc ON pg_aggregate.aggfnoid = pg_proc.oid "; + query += "INNER JOIN pg_type ON pg_type.oid=pg_proc.prorettype "; + query += "WHERE pronargs=1 AND typname IN ('int8','float8') AND aggkind='n' ORDER BY 1"; + return query; + } + } From 46c229188e985f74ed35d7505c21d9ec723d7b99 Mon Sep 17 00:00:00 2001 From: Sven Klemm Date: Wed, 14 Mar 2018 23:03:32 +0100 Subject: [PATCH 032/786] read aggregate functions from database --- .../datasource/postgres/postgres_query.ts | 3 ++- .../plugins/datasource/postgres/query_ctrl.ts | 11 +++++++++++ .../plugins/datasource/postgres/query_part.ts | 17 +++++++++++++++++ 3 files changed, 30 insertions(+), 1 deletion(-) diff --git a/public/app/plugins/datasource/postgres/postgres_query.ts b/public/app/plugins/datasource/postgres/postgres_query.ts index 8f9f1261340..4516bf4a4be 100644 --- a/public/app/plugins/datasource/postgres/postgres_query.ts +++ b/public/app/plugins/datasource/postgres/postgres_query.ts @@ -24,9 +24,10 @@ export default class PostgresQuery { target.where = target.where || []; target.select = target.select || [[{ type: 'column', params: ['value'] }]]; - this.updateProjection(); // give interpolateQueryStr access to this this.interpolateQueryStr = this.interpolateQueryStr.bind(this); + + this.updateProjection(); } quoteIdentifier(value) { diff --git a/public/app/plugins/datasource/postgres/query_ctrl.ts b/public/app/plugins/datasource/postgres/query_ctrl.ts index b84b0ce0750..4a2a0ce6c1c 100644 --- a/public/app/plugins/datasource/postgres/query_ctrl.ts +++ b/public/app/plugins/datasource/postgres/query_ctrl.ts @@ -77,9 +77,19 @@ export class PostgresQueryCtrl extends QueryCtrl { }); this.panelCtrl.events.on('data-received', this.onDataReceived.bind(this), $scope); this.panelCtrl.events.on('data-error', this.onDataError.bind(this), $scope); + } buildSelectMenu() { + + if (!queryPart.hasAggregates()) { + this.datasource.metricFindQuery(this.queryBuilder.buildAggregateQuery()) + .then(results => { + queryPart.clearAggregates(); + _.map(results, segment => { queryPart.registerAggregate(segment.text); }); + }) + .catch(this.handleQueryError.bind(this)); + } var categories = queryPart.getCategories(); this.selectMenu = _.reduce( categories, @@ -279,6 +289,7 @@ export class PostgresQueryCtrl extends QueryCtrl { var datatype = results[0].text; switch (datatype) { case "text": + case "character": case "character varying": return this.$q.when(this.uiSegmentSrv.newOperators(['=', '!=', '~', '~*','!~','!~*','IN'])); default: diff --git a/public/app/plugins/datasource/postgres/query_part.ts b/public/app/plugins/datasource/postgres/query_part.ts index 600723be00d..b63ebfae0c1 100644 --- a/public/app/plugins/datasource/postgres/query_part.ts +++ b/public/app/plugins/datasource/postgres/query_part.ts @@ -23,6 +23,17 @@ function register(options: any) { options.category.push(index[options.type]); } +function registerAggregate(name: string) { + register({ + type: name, + addStrategy: replaceAggregationAddStrategy, + category: categories.Aggregations, + params: [], + defaultParams: [], + renderer: functionRenderer, + }); +} + var groupByTimeFunctions = []; function aliasRenderer(part, innerExpr) { @@ -192,6 +203,12 @@ register({ export default { create: createPart, + registerAggregate: registerAggregate, + clearAggregates: function() { categories.Aggregations = []; }, + hasAggregates: function() { + // FIXME + return categories.Aggregations.length > 6; + }, getCategories: function() { return categories; }, From 12600a0e959866036058092d35f6b7414e98dd65 Mon Sep 17 00:00:00 2001 From: Sven Klemm Date: Mon, 26 Mar 2018 13:19:14 +0200 Subject: [PATCH 033/786] support non-nested menu entries --- public/app/plugins/datasource/postgres/query_ctrl.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/public/app/plugins/datasource/postgres/query_ctrl.ts b/public/app/plugins/datasource/postgres/query_ctrl.ts index 4a2a0ce6c1c..8a5cb273ef7 100644 --- a/public/app/plugins/datasource/postgres/query_ctrl.ts +++ b/public/app/plugins/datasource/postgres/query_ctrl.ts @@ -205,7 +205,11 @@ export class PostgresQueryCtrl extends QueryCtrl { } addSelectPart(selectParts, cat, subitem) { - this.queryModel.addSelectPart(selectParts, subitem.value); + if ("submenu" in cat) { + this.queryModel.addSelectPart(selectParts, subitem.value); + } else { + this.queryModel.addSelectPart(selectParts, cat.value); + } this.panelCtrl.refresh(); } From 6c2ef7dca6b34f189ef44c416e98c386117d6010 Mon Sep 17 00:00:00 2001 From: Sven Klemm Date: Mon, 26 Mar 2018 18:34:40 +0200 Subject: [PATCH 034/786] handle aggregate functions more generic --- .../plugins/datasource/postgres/query_ctrl.ts | 47 +++++------- .../plugins/datasource/postgres/query_part.ts | 75 ++----------------- 2 files changed, 27 insertions(+), 95 deletions(-) diff --git a/public/app/plugins/datasource/postgres/query_ctrl.ts b/public/app/plugins/datasource/postgres/query_ctrl.ts index 8a5cb273ef7..d0ec5dc59be 100644 --- a/public/app/plugins/datasource/postgres/query_ctrl.ts +++ b/public/app/plugins/datasource/postgres/query_ctrl.ts @@ -81,30 +81,12 @@ export class PostgresQueryCtrl extends QueryCtrl { } buildSelectMenu() { - - if (!queryPart.hasAggregates()) { - this.datasource.metricFindQuery(this.queryBuilder.buildAggregateQuery()) - .then(results => { - queryPart.clearAggregates(); - _.map(results, segment => { queryPart.registerAggregate(segment.text); }); - }) - .catch(this.handleQueryError.bind(this)); - } - var categories = queryPart.getCategories(); - this.selectMenu = _.reduce( - categories, - function(memo, cat, key) { - var menu = { - text: key, - submenu: cat.map(item => { - return { text: item.type, value: item.type }; - }), - }; - memo.push(menu); - return memo; - }, - [] - ); + this.selectMenu = [ + {text: "aggregate", value: "aggregate"}, + {text: "math", value: "math"}, + {text: "alias", value: "alias"}, + {text: "column", value: "column"}, + ]; } toggleEditorMode() { @@ -216,10 +198,19 @@ export class PostgresQueryCtrl extends QueryCtrl { handleSelectPartEvent(selectParts, part, evt) { switch (evt.name) { case 'get-param-options': { - return this.datasource - .metricFindQuery(this.queryBuilder.buildColumnQuery("value")) - .then(this.transformToSegments(true)) - .catch(this.handleQueryError.bind(this)); + switch (part.def.type) { + case "aggregate": + return this.datasource + .metricFindQuery(this.queryBuilder.buildAggregateQuery()) + .then(this.transformToSegments(false)) + .catch(this.handleQueryError.bind(this)); + case "column": + return this.datasource + .metricFindQuery(this.queryBuilder.buildColumnQuery("value")) + .then(this.transformToSegments(true)) + .catch(this.handleQueryError.bind(this)); + } + } case 'part-param-changed': { this.panelCtrl.refresh(); diff --git a/public/app/plugins/datasource/postgres/query_part.ts b/public/app/plugins/datasource/postgres/query_part.ts index b63ebfae0c1..044f51a2457 100644 --- a/public/app/plugins/datasource/postgres/query_part.ts +++ b/public/app/plugins/datasource/postgres/query_part.ts @@ -23,23 +23,16 @@ function register(options: any) { options.category.push(index[options.type]); } -function registerAggregate(name: string) { - register({ - type: name, - addStrategy: replaceAggregationAddStrategy, - category: categories.Aggregations, - params: [], - defaultParams: [], - renderer: functionRenderer, - }); -} - var groupByTimeFunctions = []; function aliasRenderer(part, innerExpr) { return innerExpr + ' AS ' + '"' + part.params[0] + '"'; } +function aggregateRenderer(part, innerExpr) { + return part.params[0] + '(' + innerExpr + ')'; +} + function columnRenderer(part, innerExpr) { return '"' + part.params[0] + '"'; } @@ -108,59 +101,13 @@ register({ renderer: columnRenderer, }); -// Aggregations register({ - type: 'avg', + type: 'aggregate', addStrategy: replaceAggregationAddStrategy, category: categories.Aggregations, - params: [], - defaultParams: [], - renderer: functionRenderer, -}); - -register({ - type: 'count', - addStrategy: replaceAggregationAddStrategy, - category: categories.Aggregations, - params: [], - defaultParams: [], - renderer: functionRenderer, -}); - -register({ - type: 'sum', - addStrategy: replaceAggregationAddStrategy, - category: categories.Aggregations, - params: [], - defaultParams: [], - renderer: functionRenderer, -}); - -register({ - type: 'stddev', - addStrategy: replaceAggregationAddStrategy, - category: categories.Aggregations, - params: [], - defaultParams: [], - renderer: functionRenderer, -}); - -register({ - type: 'min', - addStrategy: replaceAggregationAddStrategy, - category: categories.Aggregations, - params: [], - defaultParams: [], - renderer: functionRenderer, -}); - -register({ - type: 'max', - addStrategy: replaceAggregationAddStrategy, - category: categories.Aggregations, - params: [], - defaultParams: [], - renderer: functionRenderer, + params: [{name: 'name', type: 'string', dynamicLookup: true}], + defaultParams: ['avg'], + renderer: aggregateRenderer, }); register({ @@ -203,12 +150,6 @@ register({ export default { create: createPart, - registerAggregate: registerAggregate, - clearAggregates: function() { categories.Aggregations = []; }, - hasAggregates: function() { - // FIXME - return categories.Aggregations.length > 6; - }, getCategories: function() { return categories; }, From d6ac7aee899db14d2306ab9828cb39ea9854d853 Mon Sep 17 00:00:00 2001 From: Sven Klemm Date: Mon, 26 Mar 2018 18:50:03 +0200 Subject: [PATCH 035/786] remove unused import --- public/app/plugins/datasource/postgres/query_ctrl.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/public/app/plugins/datasource/postgres/query_ctrl.ts b/public/app/plugins/datasource/postgres/query_ctrl.ts index d0ec5dc59be..dd1da1c75cf 100644 --- a/public/app/plugins/datasource/postgres/query_ctrl.ts +++ b/public/app/plugins/datasource/postgres/query_ctrl.ts @@ -2,7 +2,6 @@ import angular from 'angular'; import _ from 'lodash'; import { PostgresQueryBuilder } from './query_builder'; import { QueryCtrl } from 'app/plugins/sdk'; -import queryPart from './query_part'; import PostgresQuery from './postgres_query'; export interface QueryMeta { From 8b3c3081689236be24a21645ca852a928d33d9c7 Mon Sep 17 00:00:00 2001 From: Sven Klemm Date: Mon, 26 Mar 2018 20:15:16 +0200 Subject: [PATCH 036/786] remove categories from queryPart --- .../datasource/postgres/postgres_query.ts | 5 +---- .../plugins/datasource/postgres/query_part.ts | 19 +------------------ 2 files changed, 2 insertions(+), 22 deletions(-) diff --git a/public/app/plugins/datasource/postgres/postgres_query.ts b/public/app/plugins/datasource/postgres/postgres_query.ts index 4516bf4a4be..a804cb70294 100644 --- a/public/app/plugins/datasource/postgres/postgres_query.ts +++ b/public/app/plugins/datasource/postgres/postgres_query.ts @@ -82,14 +82,11 @@ export default class PostgresQuery { } removeGroupByPart(part, index) { - var categories = queryPart.getCategories(); - if (part.def.type === 'time') { // remove aggregations this.target.select = _.map(this.target.select, (s: any) => { return _.filter(s, (part: any) => { - var partModel = queryPart.create(part); - if (partModel.def.category === categories.Aggregations) { + if (part.type === "aggregate") { return false; } return true; diff --git a/public/app/plugins/datasource/postgres/query_part.ts b/public/app/plugins/datasource/postgres/query_part.ts index 044f51a2457..0d2b365fe66 100644 --- a/public/app/plugins/datasource/postgres/query_part.ts +++ b/public/app/plugins/datasource/postgres/query_part.ts @@ -2,12 +2,6 @@ import _ from 'lodash'; import { QueryPartDef, QueryPart, functionRenderer, suffixRenderer } from 'app/core/components/query_part/query_part'; var index = []; -var categories = { - Aggregations: [], - Math: [], - Aliasing: [], - Columns: [], -}; function createPart(part): any { var def = index[part.type]; @@ -20,11 +14,8 @@ function createPart(part): any { function register(options: any) { index[options.type] = new QueryPartDef(options); - options.category.push(index[options.type]); } -var groupByTimeFunctions = []; - function aliasRenderer(part, innerExpr) { return innerExpr + ' AS ' + '"' + part.params[0] + '"'; } @@ -41,7 +32,7 @@ function replaceAggregationAddStrategy(selectParts, partModel) { // look for existing aggregation for (var i = 0; i < selectParts.length; i++) { var part = selectParts[i]; - if (part.def.category === categories.Aggregations) { + if (part.def.type === "aggregate") { selectParts[i] = partModel; return; } @@ -95,7 +86,6 @@ function addColumnStrategy(selectParts, partModel, query) { register({ type: 'column', addStrategy: addColumnStrategy, - category: categories.Columns, params: [{ type: 'column', dynamicLookup: true }], defaultParams: ['value'], renderer: columnRenderer, @@ -104,7 +94,6 @@ register({ register({ type: 'aggregate', addStrategy: replaceAggregationAddStrategy, - category: categories.Aggregations, params: [{name: 'name', type: 'string', dynamicLookup: true}], defaultParams: ['avg'], renderer: aggregateRenderer, @@ -113,7 +102,6 @@ register({ register({ type: 'math', addStrategy: addMathStrategy, - category: categories.Math, params: [{ name: 'expr', type: 'string' }], defaultParams: [' / 100'], renderer: suffixRenderer, @@ -122,7 +110,6 @@ register({ register({ type: 'alias', addStrategy: addAliasStrategy, - category: categories.Aliasing, params: [{ name: 'name', type: 'string', quote: 'double' }], defaultParams: ['alias'], renderMode: 'suffix', @@ -131,7 +118,6 @@ register({ register({ type: 'time', - category: groupByTimeFunctions, params: [ { name: 'interval', @@ -150,7 +136,4 @@ register({ export default { create: createPart, - getCategories: function() { - return categories; - }, }; From 069012639af8ed873a776772af22ddf883ae1722 Mon Sep 17 00:00:00 2001 From: Jonathan McCall Date: Fri, 20 Apr 2018 12:17:17 -0400 Subject: [PATCH 037/786] Sort results from GetDashboardTags --- pkg/services/sqlstore/dashboard.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pkg/services/sqlstore/dashboard.go b/pkg/services/sqlstore/dashboard.go index c0848f08863..4999e40d15e 100644 --- a/pkg/services/sqlstore/dashboard.go +++ b/pkg/services/sqlstore/dashboard.go @@ -294,7 +294,8 @@ func GetDashboardTags(query *m.GetDashboardTagsQuery) error { FROM dashboard INNER JOIN dashboard_tag on dashboard_tag.dashboard_id = dashboard.id WHERE dashboard.org_id=? - GROUP BY term` + GROUP BY term + ORDER BY term` query.Result = make([]*m.DashboardTagCloudItem, 0) sess := x.Sql(sql, query.OrgId) From 731c7520b393ae7e82603032860390771b35dc7d Mon Sep 17 00:00:00 2001 From: Sven Klemm Date: Sat, 19 May 2018 15:34:48 +0200 Subject: [PATCH 038/786] return values quotes for suggestions in where expression --- public/app/plugins/datasource/postgres/query_builder.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/public/app/plugins/datasource/postgres/query_builder.ts b/public/app/plugins/datasource/postgres/query_builder.ts index 275d15492fe..3dfbf99fefd 100644 --- a/public/app/plugins/datasource/postgres/query_builder.ts +++ b/public/app/plugins/datasource/postgres/query_builder.ts @@ -39,11 +39,10 @@ export class PostgresQueryBuilder { } buildValueQuery(column: string) { - var query = "SELECT DISTINCT " + this.queryModel.quoteIdentifier(column) + "::text"; + var query = "SELECT DISTINCT quote_literal(" + column + ")"; query += " FROM " + this.queryModel.quoteIdentifier(this.target.schema); query += "." + this.queryModel.quoteIdentifier(this.target.table); - query += " ORDER BY " + this.queryModel.quoteIdentifier(column); - query += " LIMIT 100"; + query += " ORDER BY 1 LIMIT 100"; return query; } From b12d049f6f1b24ff733b0e6f71f910977751e5a2 Mon Sep 17 00:00:00 2001 From: Sven Klemm Date: Sat, 19 May 2018 17:21:53 +0200 Subject: [PATCH 039/786] quote column name in buildValueQuery --- public/app/plugins/datasource/postgres/query_builder.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/app/plugins/datasource/postgres/query_builder.ts b/public/app/plugins/datasource/postgres/query_builder.ts index 3dfbf99fefd..dd6f95b550c 100644 --- a/public/app/plugins/datasource/postgres/query_builder.ts +++ b/public/app/plugins/datasource/postgres/query_builder.ts @@ -39,7 +39,7 @@ export class PostgresQueryBuilder { } buildValueQuery(column: string) { - var query = "SELECT DISTINCT quote_literal(" + column + ")"; + var query = "SELECT DISTINCT quote_literal(" + this.queryModel.quoteIdentifier(column) + ")"; query += " FROM " + this.queryModel.quoteIdentifier(this.target.schema); query += "." + this.queryModel.quoteIdentifier(this.target.table); query += " ORDER BY 1 LIMIT 100"; From 181dfdba04d6ff31ae468eea11a5fe334290f617 Mon Sep 17 00:00:00 2001 From: Sven Klemm Date: Mon, 21 May 2018 09:37:04 +0200 Subject: [PATCH 040/786] add sql_part component --- .../app/core/components/sql_part/sql_part.ts | 118 +++++++++++ .../components/sql_part/sql_part_editor.ts | 185 ++++++++++++++++++ public/app/core/core.ts | 2 + 3 files changed, 305 insertions(+) create mode 100644 public/app/core/components/sql_part/sql_part.ts create mode 100644 public/app/core/components/sql_part/sql_part_editor.ts diff --git a/public/app/core/components/sql_part/sql_part.ts b/public/app/core/components/sql_part/sql_part.ts new file mode 100644 index 00000000000..77366e6d09b --- /dev/null +++ b/public/app/core/components/sql_part/sql_part.ts @@ -0,0 +1,118 @@ +import _ from 'lodash'; + +export class SqlPartDef { + type: string; + params: any[]; + defaultParams: any[]; + renderer: any; + category: any; + addStrategy: any; + + constructor(options: any) { + this.type = options.type; + this.params = options.params; + this.defaultParams = options.defaultParams; + this.renderer = options.renderer; + this.category = options.category; + this.addStrategy = options.addStrategy; + } +} + +export class SqlPart { + part: any; + def: SqlPartDef; + params: any[]; + text: string; + + constructor(part: any, def: any) { + this.part = part; + this.def = def; + if (!this.def) { + throw { message: 'Could not find query part ' + part.type }; + } + + part.params = part.params || _.clone(this.def.defaultParams); + this.params = part.params; + this.updateText(); + } + + render(innerExpr: string) { + return this.def.renderer(this, innerExpr); + } + + hasMultipleParamsInString(strValue, index) { + if (strValue.indexOf(',') === -1) { + return false; + } + + return this.def.params[index + 1] && this.def.params[index + 1].optional; + } + + updateParam(strValue, index) { + // handle optional parameters + // if string contains ',' and next param is optional, split and update both + if (this.hasMultipleParamsInString(strValue, index)) { + _.each(strValue.split(','), (partVal, idx) => { + this.updateParam(partVal.trim(), idx); + }); + return; + } + + if (strValue === '' && this.def.params[index].optional) { + this.params.splice(index, 1); + } else { + this.params[index] = strValue; + } + + this.part.params = this.params; + this.updateText(); + } + + updateText() { + if (this.params.length === 0) { + this.text = this.def.type + '()'; + return; + } + + var text = this.def.type + '('; + text += this.params.join(', '); + text += ')'; + this.text = text; + } +} + +export function functionRenderer(part, innerExpr) { + var str = part.def.type + '('; + var parameters = _.map(part.params, (value, index) => { + var paramType = part.def.params[index]; + if (paramType.type === 'time') { + if (value === 'auto') { + value = '$__interval'; + } + } + if (paramType.quote === 'single') { + return "'" + value + "'"; + } else if (paramType.quote === 'double') { + return '"' + value + '"'; + } + + return value; + }); + + if (innerExpr) { + parameters.unshift(innerExpr); + } + return str + parameters.join(', ') + ')'; +} + +export function suffixRenderer(part, innerExpr) { + return innerExpr + ' ' + part.params[0]; +} + +export function identityRenderer(part, innerExpr) { + return part.params[0]; +} + +export function quotedIdentityRenderer(part, innerExpr) { + return '"' + part.params[0] + '"'; +} diff --git a/public/app/core/components/sql_part/sql_part_editor.ts b/public/app/core/components/sql_part/sql_part_editor.ts new file mode 100644 index 00000000000..837f280e66d --- /dev/null +++ b/public/app/core/components/sql_part/sql_part_editor.ts @@ -0,0 +1,185 @@ +import _ from 'lodash'; +import $ from 'jquery'; +import coreModule from 'app/core/core_module'; + +var template = ` +
- - + +
@@ -72,10 +72,10 @@ GROUP BY - - +
From e93276b1f822edb434b280662fb914a1e0e4a626 Mon Sep 17 00:00:00 2001 From: Sven Klemm Date: Mon, 21 May 2018 11:10:00 +0200 Subject: [PATCH 042/786] use sql part component --- public/app/plugins/datasource/postgres/query_part.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/public/app/plugins/datasource/postgres/query_part.ts b/public/app/plugins/datasource/postgres/query_part.ts index 0d2b365fe66..d54a66783eb 100644 --- a/public/app/plugins/datasource/postgres/query_part.ts +++ b/public/app/plugins/datasource/postgres/query_part.ts @@ -1,5 +1,5 @@ import _ from 'lodash'; -import { QueryPartDef, QueryPart, functionRenderer, suffixRenderer } from 'app/core/components/query_part/query_part'; +import { SqlPartDef, SqlPart, functionRenderer, suffixRenderer } from 'app/core/components/sql_part/sql_part'; var index = []; @@ -9,11 +9,11 @@ function createPart(part): any { throw { message: 'Could not find query part ' + part.type }; } - return new QueryPart(part, def); + return new SqlPart(part, def); } function register(options: any) { - index[options.type] = new QueryPartDef(options); + index[options.type] = new SqlPartDef(options); } function aliasRenderer(part, innerExpr) { From 3af4e4e0d696ba62731bcd9b527472b5c2acef68 Mon Sep 17 00:00:00 2001 From: Sven Klemm Date: Mon, 21 May 2018 11:44:37 +0200 Subject: [PATCH 043/786] separate label in template from type --- public/app/core/components/sql_part/sql_part.ts | 2 ++ public/app/core/components/sql_part/sql_part_editor.ts | 2 +- public/app/plugins/datasource/postgres/query_part.ts | 5 +++++ 3 files changed, 8 insertions(+), 1 deletion(-) diff --git a/public/app/core/components/sql_part/sql_part.ts b/public/app/core/components/sql_part/sql_part.ts index 77366e6d09b..94dd0ff6ae8 100644 --- a/public/app/core/components/sql_part/sql_part.ts +++ b/public/app/core/components/sql_part/sql_part.ts @@ -2,6 +2,7 @@ import _ from 'lodash'; export class SqlPartDef { type: string; + label: string; params: any[]; defaultParams: any[]; renderer: any; @@ -10,6 +11,7 @@ export class SqlPartDef { constructor(options: any) { this.type = options.type; + this.label = options.label; this.params = options.params; this.defaultParams = options.defaultParams; this.renderer = options.renderer; diff --git a/public/app/core/components/sql_part/sql_part_editor.ts b/public/app/core/components/sql_part/sql_part_editor.ts index 837f280e66d..2ebeca5753a 100644 --- a/public/app/core/components/sql_part/sql_part_editor.ts +++ b/public/app/core/components/sql_part/sql_part_editor.ts @@ -4,7 +4,7 @@ import coreModule from 'app/core/core_module'; var template = ` +
+
+ + + + +
+ +
+
+
+ +
+
-
-
- - - - -
- -
-
-
- -
-
From 3cb0e27e1c474e5d203eb32428b2f39ee5fb3216 Mon Sep 17 00:00:00 2001 From: John Baublitz Date: Sat, 19 May 2018 16:21:00 -0400 Subject: [PATCH 052/786] Revert changes post code review and move them to notification page --- pkg/api/dtos/alerting.go | 25 +++---- pkg/models/alert.go | 9 --- pkg/models/alert_notifications.go | 71 ++++++++++++++----- pkg/services/alerting/eval_context.go | 15 ++++ pkg/services/alerting/extractor.go | 2 - pkg/services/alerting/interfaces.go | 2 + pkg/services/alerting/notifier.go | 12 +++- .../alerting/notifiers/alertmanager.go | 2 +- pkg/services/alerting/notifiers/base.go | 25 +++++-- pkg/services/alerting/notifiers/dingding.go | 2 +- pkg/services/alerting/notifiers/discord.go | 2 +- pkg/services/alerting/notifiers/email.go | 2 +- pkg/services/alerting/notifiers/hipchat.go | 2 +- pkg/services/alerting/notifiers/kafka.go | 2 +- pkg/services/alerting/notifiers/line.go | 2 +- pkg/services/alerting/notifiers/opsgenie.go | 2 +- pkg/services/alerting/notifiers/pagerduty.go | 2 +- pkg/services/alerting/notifiers/pushover.go | 2 +- pkg/services/alerting/notifiers/sensu.go | 2 +- pkg/services/alerting/notifiers/slack.go | 2 +- pkg/services/alerting/notifiers/teams.go | 2 +- pkg/services/alerting/notifiers/telegram.go | 2 +- pkg/services/alerting/notifiers/threema.go | 2 +- pkg/services/alerting/notifiers/victorops.go | 2 +- pkg/services/alerting/notifiers/webhook.go | 2 +- pkg/services/alerting/result_handler.go | 1 - pkg/services/alerting/rule.go | 6 -- pkg/services/sqlstore/alert.go | 22 +----- pkg/services/sqlstore/alert_notification.go | 58 +++++++++++++-- pkg/services/sqlstore/migrations/alert_mig.go | 27 ++++++- .../app/features/alerting/alert_tab_ctrl.ts | 3 - .../alerting/notification_edit_ctrl.ts | 2 + .../features/alerting/partials/alert_tab.html | 3 - .../alerting/partials/notification_edit.html | 5 ++ 34 files changed, 215 insertions(+), 107 deletions(-) diff --git a/pkg/api/dtos/alerting.go b/pkg/api/dtos/alerting.go index 64dd619a4eb..5e0196c20d1 100644 --- a/pkg/api/dtos/alerting.go +++ b/pkg/api/dtos/alerting.go @@ -21,18 +21,17 @@ type AlertRule struct { ExecutionError string `json:"executionError"` Url string `json:"url"` CanEdit bool `json:"canEdit"` - NotifyOnce bool `json:"notifyOnce"` - NotifyEval uint64 `json:"notifyEval"` - NotifyFreq uint64 `json:"notifyFrequency"` } type AlertNotification struct { - Id int64 `json:"id"` - Name string `json:"name"` - Type string `json:"type"` - IsDefault bool `json:"isDefault"` - Created time.Time `json:"created"` - Updated time.Time `json:"updated"` + Id int64 `json:"id"` + Name string `json:"name"` + Type string `json:"type"` + IsDefault bool `json:"isDefault"` + NotifyOnce bool `json:"notifyOnce"` + Frequency bool `json:"frequency"` + Created time.Time `json:"created"` + Updated time.Time `json:"updated"` } type AlertTestCommand struct { @@ -62,9 +61,11 @@ type EvalMatch struct { } type NotificationTestCommand struct { - Name string `json:"name"` - Type string `json:"type"` - Settings *simplejson.Json `json:"settings"` + Name string `json:"name"` + Type string `json:"type"` + NotifyOnce bool `json:"notifyOnce"` + Frequency time.Duration `json:"frequency"` + Settings *simplejson.Json `json:"settings"` } type PauseAlertCommand struct { diff --git a/pkg/models/alert.go b/pkg/models/alert.go index 56ceeb2cbf7..fba2aa63df9 100644 --- a/pkg/models/alert.go +++ b/pkg/models/alert.go @@ -72,9 +72,6 @@ type Alert struct { Silenced bool ExecutionError string Frequency int64 - NotifyOnce bool - NotifyFreq uint64 - NotifyEval uint64 EvalData *simplejson.Json NewStateDate time.Time @@ -98,8 +95,6 @@ func (this *Alert) ContainsUpdates(other *Alert) bool { result := false result = result || this.Name != other.Name result = result || this.Message != other.Message - result = result || this.NotifyOnce != other.NotifyOnce - result = result || (!other.NotifyOnce && this.NotifyFreq != other.NotifyFreq) if this.Settings != nil && other.Settings != nil { json1, err1 := this.Settings.Encode() @@ -164,10 +159,6 @@ type SetAlertStateCommand struct { Timestamp time.Time } -type IncAlertEvalCommand struct { - AlertId int64 -} - //Queries type GetAlertsQuery struct { OrgId int64 diff --git a/pkg/models/alert_notifications.go b/pkg/models/alert_notifications.go index 87b515f370c..cba62a51527 100644 --- a/pkg/models/alert_notifications.go +++ b/pkg/models/alert_notifications.go @@ -7,32 +7,38 @@ import ( ) type AlertNotification struct { - Id int64 `json:"id"` - OrgId int64 `json:"-"` - Name string `json:"name"` - Type string `json:"type"` - IsDefault bool `json:"isDefault"` - Settings *simplejson.Json `json:"settings"` - Created time.Time `json:"created"` - Updated time.Time `json:"updated"` + Id int64 `json:"id"` + OrgId int64 `json:"-"` + Name string `json:"name"` + Type string `json:"type"` + NotifyOnce bool `json:"notifyOnce"` + Frequency time.Duration `json:"frequency"` + IsDefault bool `json:"isDefault"` + Settings *simplejson.Json `json:"settings"` + Created time.Time `json:"created"` + Updated time.Time `json:"updated"` } type CreateAlertNotificationCommand struct { - Name string `json:"name" binding:"Required"` - Type string `json:"type" binding:"Required"` - IsDefault bool `json:"isDefault"` - Settings *simplejson.Json `json:"settings"` + Name string `json:"name" binding:"Required"` + Type string `json:"type" binding:"Required"` + NotifyOnce bool `json:"notifyOnce" binding:"Required"` + Frequency time.Duration `json:"frequency"` + IsDefault bool `json:"isDefault"` + Settings *simplejson.Json `json:"settings"` OrgId int64 `json:"-"` Result *AlertNotification } type UpdateAlertNotificationCommand struct { - Id int64 `json:"id" binding:"Required"` - Name string `json:"name" binding:"Required"` - Type string `json:"type" binding:"Required"` - IsDefault bool `json:"isDefault"` - Settings *simplejson.Json `json:"settings" binding:"Required"` + Id int64 `json:"id" binding:"Required"` + Name string `json:"name" binding:"Required"` + Type string `json:"type" binding:"Required"` + NotifyOnce string `json:"notifyOnce" binding:"Required"` + Frequency string `json:"frequency"` + IsDefault bool `json:"isDefault"` + Settings *simplejson.Json `json:"settings" binding:"Required"` OrgId int64 `json:"-"` Result *AlertNotification @@ -63,3 +69,34 @@ type GetAllAlertNotificationsQuery struct { Result []*AlertNotification } + +type NotificationJournal struct { + Id int64 + OrgId int64 + AlertId int64 + NotifierId int64 + SentAt time.Time + Success bool +} + +type RecordNotificationJournalCommand struct { + OrgId int64 + AlertId int64 + NotifierId int64 + SentAt time.Time + Success bool +} + +type GetLatestNotificationQuery struct { + OrgId int64 + AlertId int64 + NotifierId int64 + + Result *NotificationJournal +} + +type CleanNotificationJournalCommand struct { + OrgId int64 + AlertId int64 + NotifierId int64 +} diff --git a/pkg/services/alerting/eval_context.go b/pkg/services/alerting/eval_context.go index d0441d379b7..b451d188a64 100644 --- a/pkg/services/alerting/eval_context.go +++ b/pkg/services/alerting/eval_context.go @@ -143,3 +143,18 @@ func (c *EvalContext) GetNewState() m.AlertStateType { return m.AlertStateOK } + +func (c *EvalContext) LastNotify(notifierId int64) *time.Time { + cmd := &m.GetLatestNotificationQuery{ + OrgId: c.Rule.OrgId, + AlertId: c.Rule.Id, + NotifierId: notifierId, + } + if err := bus.Dispatch(cmd); err != nil { + c.log.Warn("Could not determine last time alert", + c.Rule.Name, "notified") + return nil + } + + return &cmd.Result.SentAt +} diff --git a/pkg/services/alerting/extractor.go b/pkg/services/alerting/extractor.go index f820e546a93..e1c1bfacb2e 100644 --- a/pkg/services/alerting/extractor.go +++ b/pkg/services/alerting/extractor.go @@ -122,8 +122,6 @@ func (e *DashAlertExtractor) getAlertFromPanels(jsonWithPanels *simplejson.Json, Handler: jsonAlert.Get("handler").MustInt64(), Message: jsonAlert.Get("message").MustString(), Frequency: frequency, - NotifyOnce: jsonAlert.Get("notifyOnce").MustBool(), - NotifyFreq: jsonAlert.Get("notifyFrequency").MustUint64(), } for _, condition := range jsonAlert.Get("conditions").MustArray() { diff --git a/pkg/services/alerting/interfaces.go b/pkg/services/alerting/interfaces.go index 18f969ba1b9..8842b35fba2 100644 --- a/pkg/services/alerting/interfaces.go +++ b/pkg/services/alerting/interfaces.go @@ -19,6 +19,8 @@ type Notifier interface { GetNotifierId() int64 GetIsDefault() bool + GetNotifyOnce() bool + GetFrequency() time.Duration } type NotifierSlice []Notifier diff --git a/pkg/services/alerting/notifier.go b/pkg/services/alerting/notifier.go index 2ea68cf5085..53923a420fe 100644 --- a/pkg/services/alerting/notifier.go +++ b/pkg/services/alerting/notifier.go @@ -66,7 +66,17 @@ func (n *notificationService) sendNotifications(context *EvalContext, notifiers not := notifier //avoid updating scope variable in go routine n.log.Debug("Sending notification", "type", not.GetType(), "id", not.GetNotifierId(), "isDefault", not.GetIsDefault()) metrics.M_Alerting_Notification_Sent.WithLabelValues(not.GetType()).Inc() - g.Go(func() error { return not.Notify(context) }) + g.Go(func() error { + success := not.Notify(context) == nil + cmd := &m.RecordNotificationJournalCommand{ + OrgId: context.Rule.OrgId, + AlertId: context.Rule.Id, + NotifierId: not.GetNotifierId(), + SentAt: time.Now(), + Success: success, + } + return bus.Dispatch(cmd) + }) } return g.Wait() diff --git a/pkg/services/alerting/notifiers/alertmanager.go b/pkg/services/alerting/notifiers/alertmanager.go index d449167de13..3eeb25986e0 100644 --- a/pkg/services/alerting/notifiers/alertmanager.go +++ b/pkg/services/alerting/notifiers/alertmanager.go @@ -33,7 +33,7 @@ func NewAlertmanagerNotifier(model *m.AlertNotification) (alerting.Notifier, err } return &AlertmanagerNotifier{ - NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.Settings), + NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.NotifyOnce, model.Frequency, model.Settings), Url: url, log: log.New("alerting.notifier.prometheus-alertmanager"), }, nil diff --git a/pkg/services/alerting/notifiers/base.go b/pkg/services/alerting/notifiers/base.go index 498bae3a6e6..e9e32020c6d 100644 --- a/pkg/services/alerting/notifiers/base.go +++ b/pkg/services/alerting/notifiers/base.go @@ -1,6 +1,8 @@ package notifiers import ( + "time" + "github.com/grafana/grafana/pkg/components/simplejson" m "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/alerting" @@ -12,9 +14,11 @@ type NotifierBase struct { Id int64 IsDeault bool UploadImage bool + NotifyOnce bool + Frequency time.Duration } -func NewNotifierBase(id int64, isDefault bool, name, notifierType string, model *simplejson.Json) NotifierBase { +func NewNotifierBase(id int64, isDefault bool, name, notifierType string, notifyOnce bool, frequency time.Duration, model *simplejson.Json) NotifierBase { uploadImage := true value, exist := model.CheckGet("uploadImage") if exist { @@ -27,15 +31,17 @@ func NewNotifierBase(id int64, isDefault bool, name, notifierType string, model IsDeault: isDefault, Type: notifierType, UploadImage: uploadImage, + NotifyOnce: notifyOnce, + Frequency: frequency, } } -func defaultShouldNotify(context *alerting.EvalContext) bool { +func defaultShouldNotify(context *alerting.EvalContext, notifyOnce bool, frequency time.Duration, lastNotify *time.Time) bool { // Only notify on state change. - if context.PrevAlertState == context.Rule.State && context.Rule.NotifyOnce { + if context.PrevAlertState == context.Rule.State && notifyOnce { return false } - if !context.Rule.NotifyOnce && context.Rule.NotifyEval != 0 { + if !notifyOnce && lastNotify != nil && lastNotify.Add(frequency).After(time.Now()) { return false } // Do not notify when we become OK for the first time. @@ -46,7 +52,8 @@ func defaultShouldNotify(context *alerting.EvalContext) bool { } func (n *NotifierBase) ShouldNotify(context *alerting.EvalContext) bool { - return defaultShouldNotify(context) + lastNotify := context.LastNotify(n.Id) + return defaultShouldNotify(context, n.NotifyOnce, n.Frequency, lastNotify) } func (n *NotifierBase) GetType() string { @@ -64,3 +71,11 @@ func (n *NotifierBase) GetNotifierId() int64 { func (n *NotifierBase) GetIsDefault() bool { return n.IsDeault } + +func (n *NotifierBase) GetNotifyOnce() bool { + return n.NotifyOnce +} + +func (n *NotifierBase) GetFrequency() time.Duration { + return n.Frequency +} diff --git a/pkg/services/alerting/notifiers/dingding.go b/pkg/services/alerting/notifiers/dingding.go index 14eacef5831..78446c56f88 100644 --- a/pkg/services/alerting/notifiers/dingding.go +++ b/pkg/services/alerting/notifiers/dingding.go @@ -32,7 +32,7 @@ func NewDingDingNotifier(model *m.AlertNotification) (alerting.Notifier, error) } return &DingDingNotifier{ - NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.Settings), + NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.NotifyOnce, model.Frequency, model.Settings), Url: url, log: log.New("alerting.notifier.dingding"), }, nil diff --git a/pkg/services/alerting/notifiers/discord.go b/pkg/services/alerting/notifiers/discord.go index 3ffa7484870..693ed31e206 100644 --- a/pkg/services/alerting/notifiers/discord.go +++ b/pkg/services/alerting/notifiers/discord.go @@ -39,7 +39,7 @@ func NewDiscordNotifier(model *m.AlertNotification) (alerting.Notifier, error) { } return &DiscordNotifier{ - NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.Settings), + NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.NotifyOnce, model.Frequency, model.Settings), WebhookURL: url, log: log.New("alerting.notifier.discord"), }, nil diff --git a/pkg/services/alerting/notifiers/email.go b/pkg/services/alerting/notifiers/email.go index 562ffbe1269..234a4f8e756 100644 --- a/pkg/services/alerting/notifiers/email.go +++ b/pkg/services/alerting/notifiers/email.go @@ -52,7 +52,7 @@ func NewEmailNotifier(model *m.AlertNotification) (alerting.Notifier, error) { }) return &EmailNotifier{ - NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.Settings), + NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.NotifyOnce, model.Frequency, model.Settings), Addresses: addresses, log: log.New("alerting.notifier.email"), }, nil diff --git a/pkg/services/alerting/notifiers/hipchat.go b/pkg/services/alerting/notifiers/hipchat.go index 58e1b7bd71e..4eb5b78811e 100644 --- a/pkg/services/alerting/notifiers/hipchat.go +++ b/pkg/services/alerting/notifiers/hipchat.go @@ -59,7 +59,7 @@ func NewHipChatNotifier(model *models.AlertNotification) (alerting.Notifier, err roomId := model.Settings.Get("roomid").MustString() return &HipChatNotifier{ - NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.Settings), + NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.NotifyOnce, model.Frequency, model.Settings), Url: url, ApiKey: apikey, RoomId: roomId, diff --git a/pkg/services/alerting/notifiers/kafka.go b/pkg/services/alerting/notifiers/kafka.go index 92f6489106b..0dab556d5e1 100644 --- a/pkg/services/alerting/notifiers/kafka.go +++ b/pkg/services/alerting/notifiers/kafka.go @@ -43,7 +43,7 @@ func NewKafkaNotifier(model *m.AlertNotification) (alerting.Notifier, error) { } return &KafkaNotifier{ - NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.Settings), + NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.NotifyOnce, model.Frequency, model.Settings), Endpoint: endpoint, Topic: topic, log: log.New("alerting.notifier.kafka"), diff --git a/pkg/services/alerting/notifiers/line.go b/pkg/services/alerting/notifiers/line.go index 4814662f3a9..0ee252e6447 100644 --- a/pkg/services/alerting/notifiers/line.go +++ b/pkg/services/alerting/notifiers/line.go @@ -39,7 +39,7 @@ func NewLINENotifier(model *m.AlertNotification) (alerting.Notifier, error) { } return &LineNotifier{ - NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.Settings), + NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.NotifyOnce, model.Frequency, model.Settings), Token: token, log: log.New("alerting.notifier.line"), }, nil diff --git a/pkg/services/alerting/notifiers/opsgenie.go b/pkg/services/alerting/notifiers/opsgenie.go index f0f5142cf05..991afd5ce9b 100644 --- a/pkg/services/alerting/notifiers/opsgenie.go +++ b/pkg/services/alerting/notifiers/opsgenie.go @@ -56,7 +56,7 @@ func NewOpsGenieNotifier(model *m.AlertNotification) (alerting.Notifier, error) } return &OpsGenieNotifier{ - NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.Settings), + NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.NotifyOnce, model.Frequency, model.Settings), ApiKey: apiKey, ApiUrl: apiUrl, AutoClose: autoClose, diff --git a/pkg/services/alerting/notifiers/pagerduty.go b/pkg/services/alerting/notifiers/pagerduty.go index 02219b2203d..afa0ba63eca 100644 --- a/pkg/services/alerting/notifiers/pagerduty.go +++ b/pkg/services/alerting/notifiers/pagerduty.go @@ -51,7 +51,7 @@ func NewPagerdutyNotifier(model *m.AlertNotification) (alerting.Notifier, error) } return &PagerdutyNotifier{ - NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.Settings), + NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.NotifyOnce, model.Frequency, model.Settings), Key: key, AutoResolve: autoResolve, log: log.New("alerting.notifier.pagerduty"), diff --git a/pkg/services/alerting/notifiers/pushover.go b/pkg/services/alerting/notifiers/pushover.go index cbe9e16801a..09dfd6f0f9b 100644 --- a/pkg/services/alerting/notifiers/pushover.go +++ b/pkg/services/alerting/notifiers/pushover.go @@ -99,7 +99,7 @@ func NewPushoverNotifier(model *m.AlertNotification) (alerting.Notifier, error) return nil, alerting.ValidationError{Reason: "API token not given"} } return &PushoverNotifier{ - NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.Settings), + NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.NotifyOnce, model.Frequency, model.Settings), UserKey: userKey, ApiToken: apiToken, Priority: priority, diff --git a/pkg/services/alerting/notifiers/sensu.go b/pkg/services/alerting/notifiers/sensu.go index 9f77801d458..e6b94d3223e 100644 --- a/pkg/services/alerting/notifiers/sensu.go +++ b/pkg/services/alerting/notifiers/sensu.go @@ -51,7 +51,7 @@ func NewSensuNotifier(model *m.AlertNotification) (alerting.Notifier, error) { } return &SensuNotifier{ - NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.Settings), + NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.NotifyOnce, model.Frequency, model.Settings), Url: url, User: model.Settings.Get("username").MustString(), Source: model.Settings.Get("source").MustString(), diff --git a/pkg/services/alerting/notifiers/slack.go b/pkg/services/alerting/notifiers/slack.go index a8139b62726..fbbe4b3e59d 100644 --- a/pkg/services/alerting/notifiers/slack.go +++ b/pkg/services/alerting/notifiers/slack.go @@ -78,7 +78,7 @@ func NewSlackNotifier(model *m.AlertNotification) (alerting.Notifier, error) { uploadImage := model.Settings.Get("uploadImage").MustBool(true) return &SlackNotifier{ - NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.Settings), + NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.NotifyOnce, model.Frequency, model.Settings), Url: url, Recipient: recipient, Mention: mention, diff --git a/pkg/services/alerting/notifiers/teams.go b/pkg/services/alerting/notifiers/teams.go index 7f62340d0e1..362a367e1f2 100644 --- a/pkg/services/alerting/notifiers/teams.go +++ b/pkg/services/alerting/notifiers/teams.go @@ -33,7 +33,7 @@ func NewTeamsNotifier(model *m.AlertNotification) (alerting.Notifier, error) { } return &TeamsNotifier{ - NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.Settings), + NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.NotifyOnce, model.Frequency, model.Settings), Url: url, log: log.New("alerting.notifier.teams"), }, nil diff --git a/pkg/services/alerting/notifiers/telegram.go b/pkg/services/alerting/notifiers/telegram.go index ca24c996914..97696b2290c 100644 --- a/pkg/services/alerting/notifiers/telegram.go +++ b/pkg/services/alerting/notifiers/telegram.go @@ -78,7 +78,7 @@ func NewTelegramNotifier(model *m.AlertNotification) (alerting.Notifier, error) } return &TelegramNotifier{ - NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.Settings), + NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.NotifyOnce, model.Frequency, model.Settings), BotToken: botToken, ChatID: chatId, UploadImage: uploadImage, diff --git a/pkg/services/alerting/notifiers/threema.go b/pkg/services/alerting/notifiers/threema.go index e4ffffc9108..e7fb39f27db 100644 --- a/pkg/services/alerting/notifiers/threema.go +++ b/pkg/services/alerting/notifiers/threema.go @@ -106,7 +106,7 @@ func NewThreemaNotifier(model *m.AlertNotification) (alerting.Notifier, error) { } return &ThreemaNotifier{ - NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.Settings), + NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.NotifyOnce, model.Frequency, model.Settings), GatewayID: gatewayID, RecipientID: recipientID, APISecret: apiSecret, diff --git a/pkg/services/alerting/notifiers/victorops.go b/pkg/services/alerting/notifiers/victorops.go index a753ca3cbf6..c6c1cf76047 100644 --- a/pkg/services/alerting/notifiers/victorops.go +++ b/pkg/services/alerting/notifiers/victorops.go @@ -51,7 +51,7 @@ func NewVictoropsNotifier(model *models.AlertNotification) (alerting.Notifier, e } return &VictoropsNotifier{ - NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.Settings), + NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.NotifyOnce, model.Frequency, model.Settings), URL: url, AutoResolve: autoResolve, log: log.New("alerting.notifier.victorops"), diff --git a/pkg/services/alerting/notifiers/webhook.go b/pkg/services/alerting/notifiers/webhook.go index 4c97ed2b75e..26989873e9e 100644 --- a/pkg/services/alerting/notifiers/webhook.go +++ b/pkg/services/alerting/notifiers/webhook.go @@ -47,7 +47,7 @@ func NewWebHookNotifier(model *m.AlertNotification) (alerting.Notifier, error) { } return &WebhookNotifier{ - NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.Settings), + NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.NotifyOnce, model.Frequency, model.Settings), Url: url, User: model.Settings.Get("username").MustString(), Password: model.Settings.Get("password").MustString(), diff --git a/pkg/services/alerting/result_handler.go b/pkg/services/alerting/result_handler.go index 56d299001f0..c57b28c7c3e 100644 --- a/pkg/services/alerting/result_handler.go +++ b/pkg/services/alerting/result_handler.go @@ -88,7 +88,6 @@ func (handler *DefaultResultHandler) Handle(evalContext *EvalContext) error { } } - bus.Dispatch(&m.IncAlertEvalCommand{AlertId: evalContext.Rule.Id}) handler.notifier.SendIfNeeded(evalContext) return nil diff --git a/pkg/services/alerting/rule.go b/pkg/services/alerting/rule.go index 0003fe791e3..018d138dbe4 100644 --- a/pkg/services/alerting/rule.go +++ b/pkg/services/alerting/rule.go @@ -23,9 +23,6 @@ type Rule struct { State m.AlertStateType Conditions []Condition Notifications []int64 - NotifyOnce bool - NotifyFreq uint64 - NotifyEval uint64 } type ValidationError struct { @@ -100,9 +97,6 @@ func NewRuleFromDBAlert(ruleDef *m.Alert) (*Rule, error) { model.Name = ruleDef.Name model.Message = ruleDef.Message model.Frequency = ruleDef.Frequency - model.NotifyOnce = ruleDef.NotifyOnce - model.NotifyFreq = ruleDef.NotifyFreq - model.NotifyEval = ruleDef.NotifyEval model.State = ruleDef.State model.NoDataState = m.NoDataOption(ruleDef.Settings.Get("noDataState").MustString("no_data")) model.ExecutionErrorState = m.ExecutionErrorOption(ruleDef.Settings.Get("executionErrorState").MustString("alerting")) diff --git a/pkg/services/sqlstore/alert.go b/pkg/services/sqlstore/alert.go index 9ab28be84ee..58ec7e2857a 100644 --- a/pkg/services/sqlstore/alert.go +++ b/pkg/services/sqlstore/alert.go @@ -22,7 +22,6 @@ func init() { bus.AddHandler("sql", GetAlertStatesForDashboard) bus.AddHandler("sql", PauseAlert) bus.AddHandler("sql", PauseAllAlerts) - bus.AddHandler("sql", IncAlertEval) } func GetAlertById(query *m.GetAlertByIdQuery) error { @@ -189,7 +188,7 @@ func updateAlerts(existingAlerts []*m.Alert, cmd *m.SaveAlertsCommand, sess *DBS if alertToUpdate.ContainsUpdates(alert) { alert.Updated = timeNow() alert.State = alertToUpdate.State - sess.MustCols("message", "notify_freq", "notify_once") + sess.MustCols("message") _, err := sess.Id(alert.Id).Update(alert) if err != nil { return err @@ -344,22 +343,3 @@ func GetAlertStatesForDashboard(query *m.GetAlertStatesForDashboardQuery) error return err } - -func IncAlertEval(cmd *m.IncAlertEvalCommand) error { - return inTransaction(func(sess *DBSession) error { - alert := m.Alert{} - - if _, err := sess.Id(cmd.AlertId).Get(&alert); err != nil { - return err - } - - alert.NotifyEval = (alert.NotifyEval + 1) % alert.NotifyFreq - - sess.MustCols("notify_eval") - if _, err := sess.Id(cmd.AlertId).Update(alert); err != nil { - return err - } - - return nil - }) -} diff --git a/pkg/services/sqlstore/alert_notification.go b/pkg/services/sqlstore/alert_notification.go index 651241f7714..8bb17143042 100644 --- a/pkg/services/sqlstore/alert_notification.go +++ b/pkg/services/sqlstore/alert_notification.go @@ -17,6 +17,9 @@ func init() { bus.AddHandler("sql", DeleteAlertNotification) bus.AddHandler("sql", GetAlertNotificationsToSend) bus.AddHandler("sql", GetAllAlertNotifications) + bus.AddHandler("sql", RecordNotificationJournal) + bus.AddHandler("sql", GetLatestNotification) + bus.AddHandler("sql", CleanNotificationJournal) } func DeleteAlertNotification(cmd *m.DeleteAlertNotificationCommand) error { @@ -138,13 +141,15 @@ func CreateAlertNotificationCommand(cmd *m.CreateAlertNotificationCommand) error } alertNotification := &m.AlertNotification{ - OrgId: cmd.OrgId, - Name: cmd.Name, - Type: cmd.Type, - Settings: cmd.Settings, - Created: time.Now(), - Updated: time.Now(), - IsDefault: cmd.IsDefault, + OrgId: cmd.OrgId, + Name: cmd.Name, + Type: cmd.Type, + Settings: cmd.Settings, + NotifyOnce: cmd.NotifyOnce, + Frequency: cmd.Frequency, + Created: time.Now(), + Updated: time.Now(), + IsDefault: cmd.IsDefault, } if _, err = sess.Insert(alertNotification); err != nil { @@ -192,3 +197,42 @@ func UpdateAlertNotification(cmd *m.UpdateAlertNotificationCommand) error { return nil }) } + +func RecordNotificationJournal(cmd *m.RecordNotificationJournalCommand) error { + return inTransaction(func(sess *DBSession) error { + journalEntry := &m.NotificationJournal{ + OrgId: cmd.OrgId, + AlertId: cmd.AlertId, + NotifierId: cmd.NotifierId, + SentAt: cmd.SentAt, + Success: cmd.Success, + } + + if _, err := sess.Insert(journalEntry); err != nil { + return err + } + + return nil + }) +} + +func GetLatestNotification(cmd *m.GetLatestNotificationQuery) error { + return inTransaction(func(sess *DBSession) error { + notificationJournal := &m.NotificationJournal{} + _, err := sess.OrderBy("notification_journal.sent_at").Desc().Where("notification_journal.org_id = ? AND notification_journal.alert_id = ? AND notification_journal.notifier_id = ?", cmd.OrgId, cmd.AlertId, cmd.NotifierId).Get(notificationJournal) + if err != nil { + return err + } + + cmd.Result = notificationJournal + return nil + }) +} + +func CleanNotificationJournal(cmd *m.CleanNotificationJournalCommand) error { + return inTransaction(func(sess *DBSession) error { + sql := "DELETE FROM notification_journal WHERE notification_journal.org_id = ? AND notification_journal.alert_id = ? AND notification_journal.notifier_id = ?" + _, err := sess.Exec(sql, cmd.OrgId, cmd.AlertId, cmd.NotifierId) + return err + }) +} diff --git a/pkg/services/sqlstore/migrations/alert_mig.go b/pkg/services/sqlstore/migrations/alert_mig.go index 3452e5710cc..d045f611fb2 100644 --- a/pkg/services/sqlstore/migrations/alert_mig.go +++ b/pkg/services/sqlstore/migrations/alert_mig.go @@ -29,9 +29,6 @@ func addAlertMigrations(mg *Migrator) { {Name: "state_changes", Type: DB_Int, Nullable: false}, {Name: "created", Type: DB_DateTime, Nullable: false}, {Name: "updated", Type: DB_DateTime, Nullable: false}, - {Name: "notify_once", Type: DB_Bool, Nullable: false}, - {Name: "notify_freq", Type: DB_Int, Nullable: false}, - {Name: "notify_eval", Type: DB_Int, Nullable: false}, }, Indices: []*Index{ {Cols: []string{"org_id", "id"}, Type: IndexType}, @@ -68,8 +65,32 @@ func addAlertMigrations(mg *Migrator) { mg.AddMigration("Add column is_default", NewAddColumnMigration(alert_notification, &Column{ Name: "is_default", Type: DB_Bool, Nullable: false, Default: "0", })) + mg.AddMigration("Add column frequency", NewAddColumnMigration(alert_notification, &Column{ + Name: "frequency", Type: DB_BigInt, Nullable: true, + })) + mg.AddMigration("Add column notify_once", NewAddColumnMigration(alert_notification, &Column{ + Name: "notify_once", Type: DB_Bool, Nullable: false, Default: "1", + })) mg.AddMigration("add index alert_notification org_id & name", NewAddIndexMigration(alert_notification, alert_notification.Indices[0])) + notification_journal := Table{ + Name: "notification_journal", + Columns: []*Column{ + {Name: "id", Type: DB_BigInt, IsPrimaryKey: true, IsAutoIncrement: true}, + {Name: "org_id", Type: DB_BigInt, Nullable: false}, + {Name: "alert_id", Type: DB_BigInt, Nullable: false}, + {Name: "notifier_id", Type: DB_BigInt, Nullable: false}, + {Name: "sent_at", Type: DB_DateTime, Nullable: false}, + {Name: "success", Type: DB_Bool, Nullable: false}, + }, + Indices: []*Index{ + {Cols: []string{"org_id", "alert_id", "notifier_id"}, Type: IndexType}, + }, + } + + mg.AddMigration("create notification_journal table v1", NewAddTableMigration(notification_journal)) + mg.AddMigration("add index notification_journal org_id & alert_id & notifier_id", NewAddIndexMigration(notification_journal, notification_journal.Indices[0])) + mg.AddMigration("Update alert table charset", NewTableCharsetMigration("alert", []*Column{ {Name: "name", Type: DB_NVarchar, Length: 255, Nullable: false}, {Name: "message", Type: DB_Text, Nullable: false}, diff --git a/public/app/features/alerting/alert_tab_ctrl.ts b/public/app/features/alerting/alert_tab_ctrl.ts index f0d965ae81e..79baa1e3f5a 100644 --- a/public/app/features/alerting/alert_tab_ctrl.ts +++ b/public/app/features/alerting/alert_tab_ctrl.ts @@ -167,9 +167,6 @@ export class AlertTabCtrl { alert.noDataState = alert.noDataState || 'no_data'; alert.executionErrorState = alert.executionErrorState || 'alerting'; alert.frequency = alert.frequency || '60s'; - alert.notifyFrequency = alert.notifyFrequency || 10; - alert.notifyOnce = alert.notifyOnce == null ? true : alert.notifyOnce; - alert.frequency = alert.frequency || '60s'; alert.handler = alert.handler || 1; alert.notifications = alert.notifications || []; diff --git a/public/app/features/alerting/notification_edit_ctrl.ts b/public/app/features/alerting/notification_edit_ctrl.ts index 18b1c4d1d55..2fd185bee29 100644 --- a/public/app/features/alerting/notification_edit_ctrl.ts +++ b/public/app/features/alerting/notification_edit_ctrl.ts @@ -11,6 +11,7 @@ export class AlertNotificationEditCtrl { model: any; defaults: any = { type: 'email', + notifyOnce: true, settings: { httpMethod: 'POST', autoResolve: true, @@ -102,6 +103,7 @@ export class AlertNotificationEditCtrl { var payload = { name: this.model.name, type: this.model.type, + frequency: this.model.frequency, settings: this.model.settings, }; diff --git a/public/app/features/alerting/partials/alert_tab.html b/public/app/features/alerting/partials/alert_tab.html index 084aeb2036a..cb101672aa4 100644 --- a/public/app/features/alerting/partials/alert_tab.html +++ b/public/app/features/alerting/partials/alert_tab.html @@ -31,9 +31,6 @@ Evaluate every - {{ ctrl.alert.notifyOnce ? 'Notify on state change' : 'Notify every' }} - - evaluations
diff --git a/public/app/features/alerting/partials/notification_edit.html b/public/app/features/alerting/partials/notification_edit.html index d20b9031a8f..ccdb9ef1073 100644 --- a/public/app/features/alerting/partials/notification_edit.html +++ b/public/app/features/alerting/partials/notification_edit.html @@ -18,6 +18,11 @@
+ Date: Sun, 20 May 2018 12:12:10 -0400 Subject: [PATCH 053/786] Fix multiple bugs --- pkg/api/alerting.go | 57 ++++++++++++++++--- pkg/api/dtos/alerting.go | 19 ++++--- pkg/models/alert_notifications.go | 6 +- pkg/services/alerting/eval_context.go | 4 +- pkg/services/alerting/notifiers/base.go | 1 + pkg/services/alerting/notifiers/base_test.go | 13 +++-- pkg/services/sqlstore/alert_notification.go | 28 +++++++-- .../alerting/partials/notification_edit.html | 3 +- 8 files changed, 95 insertions(+), 36 deletions(-) diff --git a/pkg/api/alerting.go b/pkg/api/alerting.go index 961fc11b2dc..c5b47270f4d 100644 --- a/pkg/api/alerting.go +++ b/pkg/api/alerting.go @@ -193,12 +193,15 @@ func GetAlertNotifications(c *m.ReqContext) Response { for _, notification := range query.Result { result = append(result, &dtos.AlertNotification{ - Id: notification.Id, - Name: notification.Name, - Type: notification.Type, - IsDefault: notification.IsDefault, - Created: notification.Created, - Updated: notification.Updated, + Id: notification.Id, + Name: notification.Name, + Type: notification.Type, + IsDefault: notification.IsDefault, + Created: notification.Created, + Updated: notification.Updated, + Frequency: notification.Frequency.String(), + NotifyOnce: notification.NotifyOnce, + Settings: notification.Settings, }) } @@ -215,7 +218,19 @@ func GetAlertNotificationByID(c *m.ReqContext) Response { return Error(500, "Failed to get alert notifications", err) } - return JSON(200, query.Result) + result := &dtos.AlertNotification{ + Id: query.Result.Id, + Name: query.Result.Name, + Type: query.Result.Type, + IsDefault: query.Result.IsDefault, + Created: query.Result.Created, + Updated: query.Result.Updated, + Frequency: query.Result.Frequency.String(), + NotifyOnce: query.Result.NotifyOnce, + Settings: query.Result.Settings, + } + + return JSON(200, result) } func CreateAlertNotification(c *m.ReqContext, cmd m.CreateAlertNotificationCommand) Response { @@ -225,7 +240,19 @@ func CreateAlertNotification(c *m.ReqContext, cmd m.CreateAlertNotificationComma return Error(500, "Failed to create alert notification", err) } - return JSON(200, cmd.Result) + result := &dtos.AlertNotification{ + Id: cmd.Result.Id, + Name: cmd.Result.Name, + Type: cmd.Result.Type, + IsDefault: cmd.Result.IsDefault, + Created: cmd.Result.Created, + Updated: cmd.Result.Updated, + Frequency: cmd.Result.Frequency.String(), + NotifyOnce: cmd.Result.NotifyOnce, + Settings: cmd.Result.Settings, + } + + return JSON(200, result) } func UpdateAlertNotification(c *m.ReqContext, cmd m.UpdateAlertNotificationCommand) Response { @@ -235,7 +262,19 @@ func UpdateAlertNotification(c *m.ReqContext, cmd m.UpdateAlertNotificationComma return Error(500, "Failed to update alert notification", err) } - return JSON(200, cmd.Result) + result := &dtos.AlertNotification{ + Id: cmd.Result.Id, + Name: cmd.Result.Name, + Type: cmd.Result.Type, + IsDefault: cmd.Result.IsDefault, + Created: cmd.Result.Created, + Updated: cmd.Result.Updated, + Frequency: cmd.Result.Frequency.String(), + NotifyOnce: cmd.Result.NotifyOnce, + Settings: cmd.Result.Settings, + } + + return JSON(200, result) } func DeleteAlertNotification(c *m.ReqContext) Response { diff --git a/pkg/api/dtos/alerting.go b/pkg/api/dtos/alerting.go index 5e0196c20d1..7d4201fba87 100644 --- a/pkg/api/dtos/alerting.go +++ b/pkg/api/dtos/alerting.go @@ -24,14 +24,15 @@ type AlertRule struct { } type AlertNotification struct { - Id int64 `json:"id"` - Name string `json:"name"` - Type string `json:"type"` - IsDefault bool `json:"isDefault"` - NotifyOnce bool `json:"notifyOnce"` - Frequency bool `json:"frequency"` - Created time.Time `json:"created"` - Updated time.Time `json:"updated"` + Id int64 `json:"id"` + Name string `json:"name"` + Type string `json:"type"` + IsDefault bool `json:"isDefault"` + NotifyOnce bool `json:"notifyOnce"` + Frequency string `json:"frequency"` + Created time.Time `json:"created"` + Updated time.Time `json:"updated"` + Settings *simplejson.Json `json:"settings"` } type AlertTestCommand struct { @@ -64,7 +65,7 @@ type NotificationTestCommand struct { Name string `json:"name"` Type string `json:"type"` NotifyOnce bool `json:"notifyOnce"` - Frequency time.Duration `json:"frequency"` + Frequency string `json:"frequency"` Settings *simplejson.Json `json:"settings"` } diff --git a/pkg/models/alert_notifications.go b/pkg/models/alert_notifications.go index cba62a51527..6715eb21395 100644 --- a/pkg/models/alert_notifications.go +++ b/pkg/models/alert_notifications.go @@ -22,8 +22,8 @@ type AlertNotification struct { type CreateAlertNotificationCommand struct { Name string `json:"name" binding:"Required"` Type string `json:"type" binding:"Required"` - NotifyOnce bool `json:"notifyOnce" binding:"Required"` - Frequency time.Duration `json:"frequency"` + NotifyOnce bool `json:"notifyOnce"` + Frequency string `json:"frequency"` IsDefault bool `json:"isDefault"` Settings *simplejson.Json `json:"settings"` @@ -35,7 +35,7 @@ type UpdateAlertNotificationCommand struct { Id int64 `json:"id" binding:"Required"` Name string `json:"name" binding:"Required"` Type string `json:"type" binding:"Required"` - NotifyOnce string `json:"notifyOnce" binding:"Required"` + NotifyOnce bool `json:"notifyOnce"` Frequency string `json:"frequency"` IsDefault bool `json:"isDefault"` Settings *simplejson.Json `json:"settings" binding:"Required"` diff --git a/pkg/services/alerting/eval_context.go b/pkg/services/alerting/eval_context.go index b451d188a64..3817f4b4a3c 100644 --- a/pkg/services/alerting/eval_context.go +++ b/pkg/services/alerting/eval_context.go @@ -151,8 +151,8 @@ func (c *EvalContext) LastNotify(notifierId int64) *time.Time { NotifierId: notifierId, } if err := bus.Dispatch(cmd); err != nil { - c.log.Warn("Could not determine last time alert", - c.Rule.Name, "notified") + c.log.Warn("Could not determine last time alert notifier fired", + "Alert name", c.Rule.Name, "Error", err) return nil } diff --git a/pkg/services/alerting/notifiers/base.go b/pkg/services/alerting/notifiers/base.go index e9e32020c6d..734b5e56b28 100644 --- a/pkg/services/alerting/notifiers/base.go +++ b/pkg/services/alerting/notifiers/base.go @@ -41,6 +41,7 @@ func defaultShouldNotify(context *alerting.EvalContext, notifyOnce bool, frequen if context.PrevAlertState == context.Rule.State && notifyOnce { return false } + // Do not notify if interval has not elapsed if !notifyOnce && lastNotify != nil && lastNotify.Add(frequency).After(time.Now()) { return false } diff --git a/pkg/services/alerting/notifiers/base_test.go b/pkg/services/alerting/notifiers/base_test.go index b7142d144cc..5f2d4989063 100644 --- a/pkg/services/alerting/notifiers/base_test.go +++ b/pkg/services/alerting/notifiers/base_test.go @@ -3,6 +3,7 @@ package notifiers import ( "context" "testing" + "time" "github.com/grafana/grafana/pkg/components/simplejson" m "github.com/grafana/grafana/pkg/models" @@ -18,19 +19,19 @@ func TestBaseNotifier(t *testing.T) { Convey("can parse false value", func() { bJson.Set("uploadImage", false) - base := NewNotifierBase(1, false, "name", "email", bJson) + base := NewNotifierBase(1, false, "name", "email", true, 0, bJson) So(base.UploadImage, ShouldBeFalse) }) Convey("can parse true value", func() { bJson.Set("uploadImage", true) - base := NewNotifierBase(1, false, "name", "email", bJson) + base := NewNotifierBase(1, false, "name", "email", true, 0, bJson) So(base.UploadImage, ShouldBeTrue) }) Convey("default value should be true for backwards compatibility", func() { - base := NewNotifierBase(1, false, "name", "email", bJson) + base := NewNotifierBase(1, false, "name", "email", true, 0, bJson) So(base.UploadImage, ShouldBeTrue) }) }) @@ -41,7 +42,8 @@ func TestBaseNotifier(t *testing.T) { State: m.AlertStatePending, }) context.Rule.State = m.AlertStateOK - So(defaultShouldNotify(context), ShouldBeFalse) + timeNow := time.Now() + So(defaultShouldNotify(context, true, 0, &timeNow), ShouldBeFalse) }) Convey("ok -> alerting", func() { @@ -49,7 +51,8 @@ func TestBaseNotifier(t *testing.T) { State: m.AlertStateOK, }) context.Rule.State = m.AlertStateAlerting - So(defaultShouldNotify(context), ShouldBeTrue) + timeNow := time.Now() + So(defaultShouldNotify(context, true, 0, &timeNow), ShouldBeTrue) }) }) }) diff --git a/pkg/services/sqlstore/alert_notification.go b/pkg/services/sqlstore/alert_notification.go index 8bb17143042..a2cfa37ce5c 100644 --- a/pkg/services/sqlstore/alert_notification.go +++ b/pkg/services/sqlstore/alert_notification.go @@ -56,7 +56,9 @@ func GetAlertNotificationsToSend(query *m.GetAlertNotificationsToSendQuery) erro alert_notification.created, alert_notification.updated, alert_notification.settings, - alert_notification.is_default + alert_notification.is_default, + alert_notification.notify_once, + alert_notification.frequency FROM alert_notification `) @@ -94,7 +96,9 @@ func getAlertNotificationInternal(query *m.GetAlertNotificationsQuery, sess *DBS alert_notification.created, alert_notification.updated, alert_notification.settings, - alert_notification.is_default + alert_notification.is_default, + alert_notification.notify_once, + alert_notification.frequency FROM alert_notification `) @@ -140,19 +144,24 @@ func CreateAlertNotificationCommand(cmd *m.CreateAlertNotificationCommand) error return fmt.Errorf("Alert notification name %s already exists", cmd.Name) } + frequency, err_convert := time.ParseDuration(cmd.Frequency) + if err_convert != nil { + return err + } + alertNotification := &m.AlertNotification{ OrgId: cmd.OrgId, Name: cmd.Name, Type: cmd.Type, Settings: cmd.Settings, NotifyOnce: cmd.NotifyOnce, - Frequency: cmd.Frequency, + Frequency: frequency, Created: time.Now(), Updated: time.Now(), IsDefault: cmd.IsDefault, } - if _, err = sess.Insert(alertNotification); err != nil { + if _, err = sess.MustCols("notify_once").Insert(alertNotification); err != nil { return err } @@ -184,8 +193,15 @@ func UpdateAlertNotification(cmd *m.UpdateAlertNotificationCommand) error { current.Name = cmd.Name current.Type = cmd.Type current.IsDefault = cmd.IsDefault + current.NotifyOnce = cmd.NotifyOnce - sess.UseBool("is_default") + frequency, err_convert := time.ParseDuration(cmd.Frequency) + if err_convert != nil { + return err + } + current.Frequency = frequency + + sess.UseBool("is_default", "notify_once") if affected, err := sess.ID(cmd.Id).Update(current); err != nil { return err @@ -219,7 +235,7 @@ func RecordNotificationJournal(cmd *m.RecordNotificationJournalCommand) error { func GetLatestNotification(cmd *m.GetLatestNotificationQuery) error { return inTransaction(func(sess *DBSession) error { notificationJournal := &m.NotificationJournal{} - _, err := sess.OrderBy("notification_journal.sent_at").Desc().Where("notification_journal.org_id = ? AND notification_journal.alert_id = ? AND notification_journal.notifier_id = ?", cmd.OrgId, cmd.AlertId, cmd.NotifierId).Get(notificationJournal) + _, err := sess.Desc("notification_journal.sent_at").Limit(1).Where("notification_journal.org_id = ? AND notification_journal.alert_id = ? AND notification_journal.notifier_id = ?", cmd.OrgId, cmd.AlertId, cmd.NotifierId).Get(notificationJournal) if err != nil { return err } diff --git a/public/app/features/alerting/partials/notification_edit.html b/public/app/features/alerting/partials/notification_edit.html index ccdb9ef1073..dd56564cb95 100644 --- a/public/app/features/alerting/partials/notification_edit.html +++ b/public/app/features/alerting/partials/notification_edit.html @@ -20,8 +20,7 @@ Date: Sun, 20 May 2018 16:08:42 -0400 Subject: [PATCH 054/786] Fix tests --- pkg/services/sqlstore/alert_notification.go | 8 +++++ .../sqlstore/alert_notification_test.go | 32 +++++++++++-------- 2 files changed, 26 insertions(+), 14 deletions(-) diff --git a/pkg/services/sqlstore/alert_notification.go b/pkg/services/sqlstore/alert_notification.go index a2cfa37ce5c..0ecd6a18818 100644 --- a/pkg/services/sqlstore/alert_notification.go +++ b/pkg/services/sqlstore/alert_notification.go @@ -144,6 +144,10 @@ func CreateAlertNotificationCommand(cmd *m.CreateAlertNotificationCommand) error return fmt.Errorf("Alert notification name %s already exists", cmd.Name) } + if cmd.Frequency == "" { + return fmt.Errorf("Alert notification frequency required") + } + frequency, err_convert := time.ParseDuration(cmd.Frequency) if err_convert != nil { return err @@ -195,6 +199,10 @@ func UpdateAlertNotification(cmd *m.UpdateAlertNotificationCommand) error { current.IsDefault = cmd.IsDefault current.NotifyOnce = cmd.NotifyOnce + if cmd.Frequency == "" { + return fmt.Errorf("Alert notification frequency required") + } + frequency, err_convert := time.ParseDuration(cmd.Frequency) if err_convert != nil { return err diff --git a/pkg/services/sqlstore/alert_notification_test.go b/pkg/services/sqlstore/alert_notification_test.go index 2dbf9de5ca8..01c6c3aebd6 100644 --- a/pkg/services/sqlstore/alert_notification_test.go +++ b/pkg/services/sqlstore/alert_notification_test.go @@ -26,10 +26,12 @@ func TestAlertNotificationSQLAccess(t *testing.T) { Convey("Can save Alert Notification", func() { cmd := &m.CreateAlertNotificationCommand{ - Name: "ops", - Type: "email", - OrgId: 1, - Settings: simplejson.New(), + Name: "ops", + Type: "email", + OrgId: 1, + NotifyOnce: true, + Frequency: "10s", + Settings: simplejson.New(), } err = CreateAlertNotificationCommand(cmd) @@ -45,11 +47,13 @@ func TestAlertNotificationSQLAccess(t *testing.T) { Convey("Can update alert notification", func() { newCmd := &m.UpdateAlertNotificationCommand{ - Name: "NewName", - Type: "webhook", - OrgId: cmd.Result.OrgId, - Settings: simplejson.New(), - Id: cmd.Result.Id, + Name: "NewName", + Type: "webhook", + OrgId: cmd.Result.OrgId, + NotifyOnce: true, + Frequency: "10s", + Settings: simplejson.New(), + Id: cmd.Result.Id, } err := UpdateAlertNotification(newCmd) So(err, ShouldBeNil) @@ -58,12 +62,12 @@ func TestAlertNotificationSQLAccess(t *testing.T) { }) Convey("Can search using an array of ids", func() { - cmd1 := m.CreateAlertNotificationCommand{Name: "nagios", Type: "webhook", OrgId: 1, Settings: simplejson.New()} - cmd2 := m.CreateAlertNotificationCommand{Name: "slack", Type: "webhook", OrgId: 1, Settings: simplejson.New()} - cmd3 := m.CreateAlertNotificationCommand{Name: "ops2", Type: "email", OrgId: 1, Settings: simplejson.New()} - cmd4 := m.CreateAlertNotificationCommand{IsDefault: true, Name: "default", Type: "email", OrgId: 1, Settings: simplejson.New()} + cmd1 := m.CreateAlertNotificationCommand{Name: "nagios", Type: "webhook", OrgId: 1, NotifyOnce: true, Frequency: "10s", Settings: simplejson.New()} + cmd2 := m.CreateAlertNotificationCommand{Name: "slack", Type: "webhook", OrgId: 1, NotifyOnce: true, Frequency: "10s", Settings: simplejson.New()} + cmd3 := m.CreateAlertNotificationCommand{Name: "ops2", Type: "email", OrgId: 1, NotifyOnce: true, Frequency: "10s", Settings: simplejson.New()} + cmd4 := m.CreateAlertNotificationCommand{IsDefault: true, Name: "default", Type: "email", OrgId: 1, NotifyOnce: true, Frequency: "10s", Settings: simplejson.New()} - otherOrg := m.CreateAlertNotificationCommand{Name: "default", Type: "email", OrgId: 2, Settings: simplejson.New()} + otherOrg := m.CreateAlertNotificationCommand{Name: "default", Type: "email", OrgId: 2, NotifyOnce: true, Frequency: "10s", Settings: simplejson.New()} So(CreateAlertNotificationCommand(&cmd1), ShouldBeNil) So(CreateAlertNotificationCommand(&cmd2), ShouldBeNil) From 5c5951bc4274f3b4ff1ea3b41507e394faaeb22f Mon Sep 17 00:00:00 2001 From: John Baublitz Date: Sun, 20 May 2018 19:01:10 -0400 Subject: [PATCH 055/786] Bug fix for repeated alerting even on OK state and add notification_journal cleanup when alert resolves --- pkg/services/alerting/engine.go | 14 ++++++++++++++ pkg/services/alerting/notifiers/base.go | 4 ++++ 2 files changed, 18 insertions(+) diff --git a/pkg/services/alerting/engine.go b/pkg/services/alerting/engine.go index 0f8e24bcef5..43f6db66771 100644 --- a/pkg/services/alerting/engine.go +++ b/pkg/services/alerting/engine.go @@ -10,7 +10,9 @@ import ( tlog "github.com/opentracing/opentracing-go/log" "github.com/benbjohnson/clock" + "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/log" + m "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/registry" "github.com/grafana/grafana/pkg/services/rendering" "github.com/grafana/grafana/pkg/setting" @@ -205,6 +207,18 @@ func (e *AlertingService) processJob(attemptID int, attemptChan chan int, cancel } evalContext.Rule.State = evalContext.GetNewState() + if evalContext.Rule.State == m.AlertStateOK && evalContext.PrevAlertState != m.AlertStateOK { + for _, notifierId := range evalContext.Rule.Notifications { + cmd := &m.CleanNotificationJournalCommand{ + AlertId: evalContext.Rule.Id, + NotifierId: notifierId, + OrgId: evalContext.Rule.OrgId, + } + if err := bus.Dispatch(cmd); err != nil { + e.log.Error("Failed to clean up old notification records", "notifier", notifierId, "alert", evalContext.Rule.Id, "Error", err) + } + } + } e.resultHandler.Handle(evalContext) span.Finish() e.log.Debug("Job Execution completed", "timeMs", evalContext.GetDurationMs(), "alertId", evalContext.Rule.Id, "name", evalContext.Rule.Name, "firing", evalContext.Firing, "attemptID", attemptID) diff --git a/pkg/services/alerting/notifiers/base.go b/pkg/services/alerting/notifiers/base.go index 734b5e56b28..7672d397491 100644 --- a/pkg/services/alerting/notifiers/base.go +++ b/pkg/services/alerting/notifiers/base.go @@ -45,6 +45,10 @@ func defaultShouldNotify(context *alerting.EvalContext, notifyOnce bool, frequen if !notifyOnce && lastNotify != nil && lastNotify.Add(frequency).After(time.Now()) { return false } + // Do not notify if alert state if OK or pending even on repeated notify + if !notifyOnce && (context.Rule.State == m.AlertStateOK || context.Rule.State == m.AlertStatePending) { + return false + } // Do not notify when we become OK for the first time. if (context.PrevAlertState == m.AlertStatePending) && (context.Rule.State == m.AlertStateOK) { return false From bdf433594add113b05b2bbd4f0381c1090fd2d6b Mon Sep 17 00:00:00 2001 From: John Baublitz Date: Fri, 25 May 2018 14:14:33 -0400 Subject: [PATCH 056/786] Implement code review changes --- pkg/models/alert_notifications.go | 5 +++++ pkg/services/alerting/engine.go | 14 -------------- pkg/services/alerting/result_handler.go | 12 ++++++++++++ pkg/services/sqlstore/alert_notification.go | 7 ++++--- .../features/alerting/notification_edit_ctrl.ts | 1 + .../alerting/partials/notification_edit.html | 15 +++++++++++---- 6 files changed, 33 insertions(+), 21 deletions(-) diff --git a/pkg/models/alert_notifications.go b/pkg/models/alert_notifications.go index 6715eb21395..ed6b8f372d1 100644 --- a/pkg/models/alert_notifications.go +++ b/pkg/models/alert_notifications.go @@ -1,11 +1,16 @@ package models import ( + "errors" "time" "github.com/grafana/grafana/pkg/components/simplejson" ) +var ( + ErrNotificationFrequencyNotFound = errors.New("Notification frequency not specified") +) + type AlertNotification struct { Id int64 `json:"id"` OrgId int64 `json:"-"` diff --git a/pkg/services/alerting/engine.go b/pkg/services/alerting/engine.go index 43f6db66771..0f8e24bcef5 100644 --- a/pkg/services/alerting/engine.go +++ b/pkg/services/alerting/engine.go @@ -10,9 +10,7 @@ import ( tlog "github.com/opentracing/opentracing-go/log" "github.com/benbjohnson/clock" - "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/log" - m "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/registry" "github.com/grafana/grafana/pkg/services/rendering" "github.com/grafana/grafana/pkg/setting" @@ -207,18 +205,6 @@ func (e *AlertingService) processJob(attemptID int, attemptChan chan int, cancel } evalContext.Rule.State = evalContext.GetNewState() - if evalContext.Rule.State == m.AlertStateOK && evalContext.PrevAlertState != m.AlertStateOK { - for _, notifierId := range evalContext.Rule.Notifications { - cmd := &m.CleanNotificationJournalCommand{ - AlertId: evalContext.Rule.Id, - NotifierId: notifierId, - OrgId: evalContext.Rule.OrgId, - } - if err := bus.Dispatch(cmd); err != nil { - e.log.Error("Failed to clean up old notification records", "notifier", notifierId, "alert", evalContext.Rule.Id, "Error", err) - } - } - } e.resultHandler.Handle(evalContext) span.Finish() e.log.Debug("Job Execution completed", "timeMs", evalContext.GetDurationMs(), "alertId", evalContext.Rule.Id, "name", evalContext.Rule.Name, "firing", evalContext.Firing, "attemptID", attemptID) diff --git a/pkg/services/alerting/result_handler.go b/pkg/services/alerting/result_handler.go index c57b28c7c3e..c4c20bd8beb 100644 --- a/pkg/services/alerting/result_handler.go +++ b/pkg/services/alerting/result_handler.go @@ -88,6 +88,18 @@ func (handler *DefaultResultHandler) Handle(evalContext *EvalContext) error { } } + if evalContext.Rule.State == m.AlertStateOK && evalContext.PrevAlertState != m.AlertStateOK { + for _, notifierId := range evalContext.Rule.Notifications { + cmd := &m.CleanNotificationJournalCommand{ + AlertId: evalContext.Rule.Id, + NotifierId: notifierId, + OrgId: evalContext.Rule.OrgId, + } + if err := bus.Dispatch(cmd); err != nil { + handler.log.Error("Failed to clean up old notification records", "notifier", notifierId, "alert", evalContext.Rule.Id, "Error", err) + } + } + } handler.notifier.SendIfNeeded(evalContext) return nil diff --git a/pkg/services/sqlstore/alert_notification.go b/pkg/services/sqlstore/alert_notification.go index 0ecd6a18818..6913009a163 100644 --- a/pkg/services/sqlstore/alert_notification.go +++ b/pkg/services/sqlstore/alert_notification.go @@ -148,8 +148,9 @@ func CreateAlertNotificationCommand(cmd *m.CreateAlertNotificationCommand) error return fmt.Errorf("Alert notification frequency required") } - frequency, err_convert := time.ParseDuration(cmd.Frequency) - if err_convert != nil { + var frequency time.Duration + frequency, err = time.ParseDuration(cmd.Frequency) + if err != nil { return err } @@ -200,7 +201,7 @@ func UpdateAlertNotification(cmd *m.UpdateAlertNotificationCommand) error { current.NotifyOnce = cmd.NotifyOnce if cmd.Frequency == "" { - return fmt.Errorf("Alert notification frequency required") + return m.ErrNotificationFrequencyNotFound } frequency, err_convert := time.ParseDuration(cmd.Frequency) diff --git a/public/app/features/alerting/notification_edit_ctrl.ts b/public/app/features/alerting/notification_edit_ctrl.ts index 2fd185bee29..9d20e871c7c 100644 --- a/public/app/features/alerting/notification_edit_ctrl.ts +++ b/public/app/features/alerting/notification_edit_ctrl.ts @@ -12,6 +12,7 @@ export class AlertNotificationEditCtrl { defaults: any = { type: 'email', notifyOnce: true, + frequency: '15m', settings: { httpMethod: 'POST', autoResolve: true, diff --git a/public/app/features/alerting/partials/notification_edit.html b/public/app/features/alerting/partials/notification_edit.html index dd56564cb95..48d44b74581 100644 --- a/public/app/features/alerting/partials/notification_edit.html +++ b/public/app/features/alerting/partials/notification_edit.html @@ -18,10 +18,6 @@ - + + +
+ Notify every + +
From 86e65f84f9ba86b202ff38d7f51f1b7d2e75f02f Mon Sep 17 00:00:00 2001 From: bergquist Date: Mon, 4 Jun 2018 17:30:57 +0200 Subject: [PATCH 057/786] alerting: fixes invalid error handling --- pkg/services/sqlstore/alert_notification.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/services/sqlstore/alert_notification.go b/pkg/services/sqlstore/alert_notification.go index 6913009a163..4f79035063e 100644 --- a/pkg/services/sqlstore/alert_notification.go +++ b/pkg/services/sqlstore/alert_notification.go @@ -204,8 +204,8 @@ func UpdateAlertNotification(cmd *m.UpdateAlertNotificationCommand) error { return m.ErrNotificationFrequencyNotFound } - frequency, err_convert := time.ParseDuration(cmd.Frequency) - if err_convert != nil { + frequency, err := time.ParseDuration(cmd.Frequency) + if err != nil { return err } current.Frequency = frequency From 93124f38fae77627cdb0a7b4a5f71171c5088eca Mon Sep 17 00:00:00 2001 From: bergquist Date: Mon, 4 Jun 2018 22:19:27 +0200 Subject: [PATCH 058/786] alerting: only check frequency when not send once --- pkg/api/alerting.go | 54 ++---------------- pkg/api/alerting_test.go | 19 +++++++ pkg/api/dtos/alerting.go | 53 +++++++++++++----- pkg/services/sqlstore/alert_notification.go | 35 +++++++----- .../sqlstore/alert_notification_test.go | 55 ++++++++++++++++++- 5 files changed, 135 insertions(+), 81 deletions(-) diff --git a/pkg/api/alerting.go b/pkg/api/alerting.go index 4e9b89fefd6..a936d696207 100644 --- a/pkg/api/alerting.go +++ b/pkg/api/alerting.go @@ -192,17 +192,7 @@ func GetAlertNotifications(c *m.ReqContext) Response { result := make([]*dtos.AlertNotification, 0) for _, notification := range query.Result { - result = append(result, &dtos.AlertNotification{ - Id: notification.Id, - Name: notification.Name, - Type: notification.Type, - IsDefault: notification.IsDefault, - Created: notification.Created, - Updated: notification.Updated, - Frequency: notification.Frequency.String(), - NotifyOnce: notification.NotifyOnce, - Settings: notification.Settings, - }) + result = append(result, dtos.NewAlertNotification(notification)) } return JSON(200, result) @@ -218,19 +208,7 @@ func GetAlertNotificationByID(c *m.ReqContext) Response { return Error(500, "Failed to get alert notifications", err) } - result := &dtos.AlertNotification{ - Id: query.Result.Id, - Name: query.Result.Name, - Type: query.Result.Type, - IsDefault: query.Result.IsDefault, - Created: query.Result.Created, - Updated: query.Result.Updated, - Frequency: query.Result.Frequency.String(), - NotifyOnce: query.Result.NotifyOnce, - Settings: query.Result.Settings, - } - - return JSON(200, result) + return JSON(200, dtos.NewAlertNotification(query.Result)) } func CreateAlertNotification(c *m.ReqContext, cmd m.CreateAlertNotificationCommand) Response { @@ -240,19 +218,7 @@ func CreateAlertNotification(c *m.ReqContext, cmd m.CreateAlertNotificationComma return Error(500, "Failed to create alert notification", err) } - result := &dtos.AlertNotification{ - Id: cmd.Result.Id, - Name: cmd.Result.Name, - Type: cmd.Result.Type, - IsDefault: cmd.Result.IsDefault, - Created: cmd.Result.Created, - Updated: cmd.Result.Updated, - Frequency: cmd.Result.Frequency.String(), - NotifyOnce: cmd.Result.NotifyOnce, - Settings: cmd.Result.Settings, - } - - return JSON(200, result) + return JSON(200, dtos.NewAlertNotification(cmd.Result)) } func UpdateAlertNotification(c *m.ReqContext, cmd m.UpdateAlertNotificationCommand) Response { @@ -262,19 +228,7 @@ func UpdateAlertNotification(c *m.ReqContext, cmd m.UpdateAlertNotificationComma return Error(500, "Failed to update alert notification", err) } - result := &dtos.AlertNotification{ - Id: cmd.Result.Id, - Name: cmd.Result.Name, - Type: cmd.Result.Type, - IsDefault: cmd.Result.IsDefault, - Created: cmd.Result.Created, - Updated: cmd.Result.Updated, - Frequency: cmd.Result.Frequency.String(), - NotifyOnce: cmd.Result.NotifyOnce, - Settings: cmd.Result.Settings, - } - - return JSON(200, result) + return JSON(200, dtos.NewAlertNotification(cmd.Result)) } func DeleteAlertNotification(c *m.ReqContext) Response { diff --git a/pkg/api/alerting_test.go b/pkg/api/alerting_test.go index abfdfb66322..3e50487190c 100644 --- a/pkg/api/alerting_test.go +++ b/pkg/api/alerting_test.go @@ -2,6 +2,7 @@ package api import ( "testing" + "time" "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/bus" @@ -11,6 +12,24 @@ import ( . "github.com/smartystreets/goconvey/convey" ) +func TestRemoveZeroUnitsFromInterval(t *testing.T) { + tcs := []struct { + interval time.Duration + expected string + }{ + {interval: time.Duration(time.Hour), expected: "1h"}, + {interval: time.Duration(time.Hour + time.Minute), expected: "1h1m"}, + {interval: time.Duration((time.Hour * 10) + time.Minute), expected: "10h1m"}, + } + + for _, tc := range tcs { + got := removeZeroesFromDuration(tc.interval) + if got != tc.expected { + t.Errorf("expected %s got %s internval: %v", tc.expected, got, tc.interval) + } + } +} + func TestAlertingApiEndpoint(t *testing.T) { Convey("Given an alert in a dashboard with an acl", t, func() { diff --git a/pkg/api/dtos/alerting.go b/pkg/api/dtos/alerting.go index 7d4201fba87..786fccc10b5 100644 --- a/pkg/api/dtos/alerting.go +++ b/pkg/api/dtos/alerting.go @@ -1,26 +1,51 @@ package dtos import ( + "strings" "time" "github.com/grafana/grafana/pkg/components/null" "github.com/grafana/grafana/pkg/components/simplejson" - m "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/models" ) type AlertRule struct { - Id int64 `json:"id"` - DashboardId int64 `json:"dashboardId"` - PanelId int64 `json:"panelId"` - Name string `json:"name"` - Message string `json:"message"` - State m.AlertStateType `json:"state"` - NewStateDate time.Time `json:"newStateDate"` - EvalDate time.Time `json:"evalDate"` - EvalData *simplejson.Json `json:"evalData"` - ExecutionError string `json:"executionError"` - Url string `json:"url"` - CanEdit bool `json:"canEdit"` + Id int64 `json:"id"` + DashboardId int64 `json:"dashboardId"` + PanelId int64 `json:"panelId"` + Name string `json:"name"` + Message string `json:"message"` + State models.AlertStateType `json:"state"` + NewStateDate time.Time `json:"newStateDate"` + EvalDate time.Time `json:"evalDate"` + EvalData *simplejson.Json `json:"evalData"` + ExecutionError string `json:"executionError"` + Url string `json:"url"` + CanEdit bool `json:"canEdit"` +} + +func removeZeroesFromDuration(interval time.Duration) string { + frequency := interval.String() + + frequency = strings.Replace(frequency, "0h", "", 1) + frequency = strings.Replace(frequency, "0m", "", 1) + frequency = strings.Replace(frequency, "0s", "", 1) + + return frequency +} + +func NewAlertNotification(notification *models.AlertNotification) *AlertNotification { + return &AlertNotification{ + Id: notification.Id, + Name: notification.Name, + Type: notification.Type, + IsDefault: notification.IsDefault, + Created: notification.Created, + Updated: notification.Updated, + Frequency: removeZeroesFromDuration(notification.Frequency), + NotifyOnce: notification.NotifyOnce, + Settings: notification.Settings, + } } type AlertNotification struct { @@ -42,7 +67,7 @@ type AlertTestCommand struct { type AlertTestResult struct { Firing bool `json:"firing"` - State m.AlertStateType `json:"state"` + State models.AlertStateType `json:"state"` ConditionEvals string `json:"conditionEvals"` TimeMs string `json:"timeMs"` Error string `json:"error,omitempty"` diff --git a/pkg/services/sqlstore/alert_notification.go b/pkg/services/sqlstore/alert_notification.go index 4f79035063e..ff36c38b1a5 100644 --- a/pkg/services/sqlstore/alert_notification.go +++ b/pkg/services/sqlstore/alert_notification.go @@ -144,14 +144,16 @@ func CreateAlertNotificationCommand(cmd *m.CreateAlertNotificationCommand) error return fmt.Errorf("Alert notification name %s already exists", cmd.Name) } - if cmd.Frequency == "" { - return fmt.Errorf("Alert notification frequency required") - } - var frequency time.Duration - frequency, err = time.ParseDuration(cmd.Frequency) - if err != nil { - return err + if !cmd.NotifyOnce { + if cmd.Frequency == "" { + return m.ErrNotificationFrequencyNotFound + } + + frequency, err = time.ParseDuration(cmd.Frequency) + if err != nil { + return err + } } alertNotification := &m.AlertNotification{ @@ -200,22 +202,25 @@ func UpdateAlertNotification(cmd *m.UpdateAlertNotificationCommand) error { current.IsDefault = cmd.IsDefault current.NotifyOnce = cmd.NotifyOnce - if cmd.Frequency == "" { - return m.ErrNotificationFrequencyNotFound - } + if !current.NotifyOnce { + if cmd.Frequency == "" { + return m.ErrNotificationFrequencyNotFound + } - frequency, err := time.ParseDuration(cmd.Frequency) - if err != nil { - return err + frequency, err := time.ParseDuration(cmd.Frequency) + if err != nil { + return err + } + + current.Frequency = frequency } - current.Frequency = frequency sess.UseBool("is_default", "notify_once") if affected, err := sess.ID(cmd.Id).Update(current); err != nil { return err } else if affected == 0 { - return fmt.Errorf("Could not find alert notification") + return fmt.Errorf("Could not update alert notification") } cmd.Result = ¤t diff --git a/pkg/services/sqlstore/alert_notification_test.go b/pkg/services/sqlstore/alert_notification_test.go index 01c6c3aebd6..578a53f34ad 100644 --- a/pkg/services/sqlstore/alert_notification_test.go +++ b/pkg/services/sqlstore/alert_notification_test.go @@ -11,7 +11,6 @@ import ( func TestAlertNotificationSQLAccess(t *testing.T) { Convey("Testing Alert notification sql access", t, func() { InitTestDB(t) - var err error Convey("Alert notifications should be empty", func() { cmd := &m.GetAlertNotificationsQuery{ @@ -24,6 +23,58 @@ func TestAlertNotificationSQLAccess(t *testing.T) { So(cmd.Result, ShouldBeNil) }) + Convey("Cannot save alert notifier with notitfyonce = false", func() { + cmd := &m.CreateAlertNotificationCommand{ + Name: "ops", + Type: "email", + OrgId: 1, + NotifyOnce: false, + Settings: simplejson.New(), + } + + Convey("and missing frequency", func() { + err := CreateAlertNotificationCommand(cmd) + So(err, ShouldEqual, m.ErrNotificationFrequencyNotFound) + }) + + Convey("invalid frequency", func() { + cmd.Frequency = "invalid duration" + + err := CreateAlertNotificationCommand(cmd) + So(err.Error(), ShouldEqual, "time: invalid duration invalid duration") + }) + }) + + Convey("Cannot update alert notifier with notitfyonce = false", func() { + cmd := &m.CreateAlertNotificationCommand{ + Name: "ops update", + Type: "email", + OrgId: 1, + NotifyOnce: true, + Settings: simplejson.New(), + } + + err := CreateAlertNotificationCommand(cmd) + So(err, ShouldBeNil) + + updateCmd := &m.UpdateAlertNotificationCommand{ + Id: cmd.Result.Id, + NotifyOnce: false, + } + + Convey("and missing frequency", func() { + err := UpdateAlertNotification(updateCmd) + So(err, ShouldEqual, m.ErrNotificationFrequencyNotFound) + }) + + Convey("invalid frequency", func() { + updateCmd.Frequency = "invalid duration" + + err := UpdateAlertNotification(updateCmd) + So(err.Error(), ShouldEqual, "time: invalid duration invalid duration") + }) + }) + Convey("Can save Alert Notification", func() { cmd := &m.CreateAlertNotificationCommand{ Name: "ops", @@ -34,7 +85,7 @@ func TestAlertNotificationSQLAccess(t *testing.T) { Settings: simplejson.New(), } - err = CreateAlertNotificationCommand(cmd) + err := CreateAlertNotificationCommand(cmd) So(err, ShouldBeNil) So(cmd.Result.Id, ShouldNotEqual, 0) So(cmd.Result.OrgId, ShouldNotEqual, 0) From 0c6d8398a14e3a4c15b10af5ad0b4eda2965d988 Mon Sep 17 00:00:00 2001 From: bergquist Date: Tue, 5 Jun 2018 08:42:39 +0200 Subject: [PATCH 059/786] alerting: remove zero units from duration --- pkg/api/alerting_test.go | 19 ------------------- pkg/api/dtos/alerting.go | 29 +++++++++++++++++++++-------- pkg/api/dtos/alerting_test.go | 34 ++++++++++++++++++++++++++++++++++ 3 files changed, 55 insertions(+), 27 deletions(-) create mode 100644 pkg/api/dtos/alerting_test.go diff --git a/pkg/api/alerting_test.go b/pkg/api/alerting_test.go index 3e50487190c..abfdfb66322 100644 --- a/pkg/api/alerting_test.go +++ b/pkg/api/alerting_test.go @@ -2,7 +2,6 @@ package api import ( "testing" - "time" "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/bus" @@ -12,24 +11,6 @@ import ( . "github.com/smartystreets/goconvey/convey" ) -func TestRemoveZeroUnitsFromInterval(t *testing.T) { - tcs := []struct { - interval time.Duration - expected string - }{ - {interval: time.Duration(time.Hour), expected: "1h"}, - {interval: time.Duration(time.Hour + time.Minute), expected: "1h1m"}, - {interval: time.Duration((time.Hour * 10) + time.Minute), expected: "10h1m"}, - } - - for _, tc := range tcs { - got := removeZeroesFromDuration(tc.interval) - if got != tc.expected { - t.Errorf("expected %s got %s internval: %v", tc.expected, got, tc.interval) - } - } -} - func TestAlertingApiEndpoint(t *testing.T) { Convey("Given an alert in a dashboard with an acl", t, func() { diff --git a/pkg/api/dtos/alerting.go b/pkg/api/dtos/alerting.go index 786fccc10b5..f8671978148 100644 --- a/pkg/api/dtos/alerting.go +++ b/pkg/api/dtos/alerting.go @@ -1,7 +1,7 @@ package dtos import ( - "strings" + "fmt" "time" "github.com/grafana/grafana/pkg/components/null" @@ -24,14 +24,27 @@ type AlertRule struct { CanEdit bool `json:"canEdit"` } -func removeZeroesFromDuration(interval time.Duration) string { - frequency := interval.String() +func formatShort(interval time.Duration) string { + var result string - frequency = strings.Replace(frequency, "0h", "", 1) - frequency = strings.Replace(frequency, "0m", "", 1) - frequency = strings.Replace(frequency, "0s", "", 1) + hours := interval / time.Hour + if hours > 0 { + result += fmt.Sprintf("%dh", hours) + } - return frequency + remaining := interval - (hours * time.Hour) + mins := remaining / time.Minute + if mins > 0 { + result += fmt.Sprintf("%dm", mins) + } + + remaining = remaining - (mins * time.Minute) + seconds := remaining / time.Second + if seconds > 0 { + result += fmt.Sprintf("%ds", seconds) + } + + return result } func NewAlertNotification(notification *models.AlertNotification) *AlertNotification { @@ -42,7 +55,7 @@ func NewAlertNotification(notification *models.AlertNotification) *AlertNotifica IsDefault: notification.IsDefault, Created: notification.Created, Updated: notification.Updated, - Frequency: removeZeroesFromDuration(notification.Frequency), + Frequency: formatShort(notification.Frequency), NotifyOnce: notification.NotifyOnce, Settings: notification.Settings, } diff --git a/pkg/api/dtos/alerting_test.go b/pkg/api/dtos/alerting_test.go new file mode 100644 index 00000000000..ea4c97fb4cc --- /dev/null +++ b/pkg/api/dtos/alerting_test.go @@ -0,0 +1,34 @@ +package dtos + +import ( + "testing" + "time" +) + +func TestFormatShort(t *testing.T) { + tcs := []struct { + interval time.Duration + expected string + }{ + {interval: time.Duration(time.Hour), expected: "1h"}, + {interval: time.Duration(time.Hour + time.Minute), expected: "1h1m"}, + {interval: time.Duration((time.Hour * 10) + time.Minute), expected: "10h1m"}, + {interval: time.Duration((time.Hour * 10) + (time.Minute * 10) + time.Second), expected: "10h10m1s"}, + } + + for _, tc := range tcs { + got := formatShort(tc.interval) + if got != tc.expected { + t.Errorf("expected %s got %s interval: %v", tc.expected, got, tc.interval) + } + + parsed, err := time.ParseDuration(tc.expected) + if err != nil { + t.Fatalf("could not parse expected duration") + } + + if parsed != tc.interval { + t.Errorf("expectes the parsed duration to equal the interval. Got %v expected: %v", parsed, tc.interval) + } + } +} From 7333d7b8d4c128092b39c1e5616977c0a6aff015 Mon Sep 17 00:00:00 2001 From: bergquist Date: Tue, 5 Jun 2018 10:27:29 +0200 Subject: [PATCH 060/786] alerting: invert sendOnce to sendReminder --- pkg/api/dtos/alerting.go | 46 +++++++------- pkg/api/dtos/alerting_test.go | 1 + pkg/models/alert_notifications.go | 46 +++++++------- pkg/services/alerting/interfaces.go | 2 +- .../alerting/notifiers/alertmanager.go | 2 +- pkg/services/alerting/notifiers/base.go | 54 ++++++++-------- pkg/services/alerting/notifiers/base_test.go | 13 +++- pkg/services/alerting/notifiers/dingding.go | 2 +- pkg/services/alerting/notifiers/discord.go | 2 +- pkg/services/alerting/notifiers/email.go | 2 +- pkg/services/alerting/notifiers/hipchat.go | 2 +- pkg/services/alerting/notifiers/kafka.go | 2 +- pkg/services/alerting/notifiers/line.go | 2 +- pkg/services/alerting/notifiers/opsgenie.go | 2 +- pkg/services/alerting/notifiers/pagerduty.go | 2 +- pkg/services/alerting/notifiers/pushover.go | 2 +- pkg/services/alerting/notifiers/sensu.go | 2 +- pkg/services/alerting/notifiers/slack.go | 2 +- pkg/services/alerting/notifiers/teams.go | 2 +- pkg/services/alerting/notifiers/telegram.go | 2 +- pkg/services/alerting/notifiers/threema.go | 2 +- pkg/services/alerting/notifiers/victorops.go | 2 +- pkg/services/alerting/notifiers/webhook.go | 2 +- pkg/services/sqlstore/alert_notification.go | 32 +++++----- .../sqlstore/alert_notification_test.go | 63 ++++++++++--------- pkg/services/sqlstore/migrations/alert_mig.go | 5 +- .../alerting/notification_edit_ctrl.ts | 2 +- .../alerting/partials/notification_edit.html | 23 +++++-- 28 files changed, 173 insertions(+), 148 deletions(-) diff --git a/pkg/api/dtos/alerting.go b/pkg/api/dtos/alerting.go index f8671978148..697d0a35a08 100644 --- a/pkg/api/dtos/alerting.go +++ b/pkg/api/dtos/alerting.go @@ -49,28 +49,28 @@ func formatShort(interval time.Duration) string { func NewAlertNotification(notification *models.AlertNotification) *AlertNotification { return &AlertNotification{ - Id: notification.Id, - Name: notification.Name, - Type: notification.Type, - IsDefault: notification.IsDefault, - Created: notification.Created, - Updated: notification.Updated, - Frequency: formatShort(notification.Frequency), - NotifyOnce: notification.NotifyOnce, - Settings: notification.Settings, + Id: notification.Id, + Name: notification.Name, + Type: notification.Type, + IsDefault: notification.IsDefault, + Created: notification.Created, + Updated: notification.Updated, + Frequency: formatShort(notification.Frequency), + SendReminder: notification.SendReminder, + Settings: notification.Settings, } } type AlertNotification struct { - Id int64 `json:"id"` - Name string `json:"name"` - Type string `json:"type"` - IsDefault bool `json:"isDefault"` - NotifyOnce bool `json:"notifyOnce"` - Frequency string `json:"frequency"` - Created time.Time `json:"created"` - Updated time.Time `json:"updated"` - Settings *simplejson.Json `json:"settings"` + Id int64 `json:"id"` + Name string `json:"name"` + Type string `json:"type"` + IsDefault bool `json:"isDefault"` + SendReminder bool `json:"sendReminder"` + Frequency string `json:"frequency"` + Created time.Time `json:"created"` + Updated time.Time `json:"updated"` + Settings *simplejson.Json `json:"settings"` } type AlertTestCommand struct { @@ -100,11 +100,11 @@ type EvalMatch struct { } type NotificationTestCommand struct { - Name string `json:"name"` - Type string `json:"type"` - NotifyOnce bool `json:"notifyOnce"` - Frequency string `json:"frequency"` - Settings *simplejson.Json `json:"settings"` + Name string `json:"name"` + Type string `json:"type"` + SendReminder bool `json:"sendReminder"` + Frequency string `json:"frequency"` + Settings *simplejson.Json `json:"settings"` } type PauseAlertCommand struct { diff --git a/pkg/api/dtos/alerting_test.go b/pkg/api/dtos/alerting_test.go index ea4c97fb4cc..bd0d9ff8feb 100644 --- a/pkg/api/dtos/alerting_test.go +++ b/pkg/api/dtos/alerting_test.go @@ -14,6 +14,7 @@ func TestFormatShort(t *testing.T) { {interval: time.Duration(time.Hour + time.Minute), expected: "1h1m"}, {interval: time.Duration((time.Hour * 10) + time.Minute), expected: "10h1m"}, {interval: time.Duration((time.Hour * 10) + (time.Minute * 10) + time.Second), expected: "10h10m1s"}, + {interval: time.Duration(time.Minute * 10), expected: "10m"}, } for _, tc := range tcs { diff --git a/pkg/models/alert_notifications.go b/pkg/models/alert_notifications.go index ed6b8f372d1..c17124dd6ef 100644 --- a/pkg/models/alert_notifications.go +++ b/pkg/models/alert_notifications.go @@ -12,38 +12,38 @@ var ( ) type AlertNotification struct { - Id int64 `json:"id"` - OrgId int64 `json:"-"` - Name string `json:"name"` - Type string `json:"type"` - NotifyOnce bool `json:"notifyOnce"` - Frequency time.Duration `json:"frequency"` - IsDefault bool `json:"isDefault"` - Settings *simplejson.Json `json:"settings"` - Created time.Time `json:"created"` - Updated time.Time `json:"updated"` + Id int64 `json:"id"` + OrgId int64 `json:"-"` + Name string `json:"name"` + Type string `json:"type"` + SendReminder bool `json:"sendReminder"` + Frequency time.Duration `json:"frequency"` + IsDefault bool `json:"isDefault"` + Settings *simplejson.Json `json:"settings"` + Created time.Time `json:"created"` + Updated time.Time `json:"updated"` } type CreateAlertNotificationCommand struct { - Name string `json:"name" binding:"Required"` - Type string `json:"type" binding:"Required"` - NotifyOnce bool `json:"notifyOnce"` - Frequency string `json:"frequency"` - IsDefault bool `json:"isDefault"` - Settings *simplejson.Json `json:"settings"` + Name string `json:"name" binding:"Required"` + Type string `json:"type" binding:"Required"` + SendReminder bool `json:"sendReminder"` + Frequency string `json:"frequency"` + IsDefault bool `json:"isDefault"` + Settings *simplejson.Json `json:"settings"` OrgId int64 `json:"-"` Result *AlertNotification } type UpdateAlertNotificationCommand struct { - Id int64 `json:"id" binding:"Required"` - Name string `json:"name" binding:"Required"` - Type string `json:"type" binding:"Required"` - NotifyOnce bool `json:"notifyOnce"` - Frequency string `json:"frequency"` - IsDefault bool `json:"isDefault"` - Settings *simplejson.Json `json:"settings" binding:"Required"` + Id int64 `json:"id" binding:"Required"` + Name string `json:"name" binding:"Required"` + Type string `json:"type" binding:"Required"` + SendReminder bool `json:"sendReminder"` + Frequency string `json:"frequency"` + IsDefault bool `json:"isDefault"` + Settings *simplejson.Json `json:"settings" binding:"Required"` OrgId int64 `json:"-"` Result *AlertNotification diff --git a/pkg/services/alerting/interfaces.go b/pkg/services/alerting/interfaces.go index 8842b35fba2..95fd4b5d04e 100644 --- a/pkg/services/alerting/interfaces.go +++ b/pkg/services/alerting/interfaces.go @@ -19,7 +19,7 @@ type Notifier interface { GetNotifierId() int64 GetIsDefault() bool - GetNotifyOnce() bool + GetSendReminder() bool GetFrequency() time.Duration } diff --git a/pkg/services/alerting/notifiers/alertmanager.go b/pkg/services/alerting/notifiers/alertmanager.go index 3eeb25986e0..42ffa9b2d6e 100644 --- a/pkg/services/alerting/notifiers/alertmanager.go +++ b/pkg/services/alerting/notifiers/alertmanager.go @@ -33,7 +33,7 @@ func NewAlertmanagerNotifier(model *m.AlertNotification) (alerting.Notifier, err } return &AlertmanagerNotifier{ - NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.NotifyOnce, model.Frequency, model.Settings), + NotifierBase: NewNotifierBase(model), Url: url, log: log.New("alerting.notifier.prometheus-alertmanager"), }, nil diff --git a/pkg/services/alerting/notifiers/base.go b/pkg/services/alerting/notifiers/base.go index 7672d397491..1d0d904457f 100644 --- a/pkg/services/alerting/notifiers/base.go +++ b/pkg/services/alerting/notifiers/base.go @@ -3,54 +3,56 @@ package notifiers import ( "time" - "github.com/grafana/grafana/pkg/components/simplejson" - m "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/alerting" ) type NotifierBase struct { - Name string - Type string - Id int64 - IsDeault bool - UploadImage bool - NotifyOnce bool - Frequency time.Duration + Name string + Type string + Id int64 + IsDeault bool + UploadImage bool + SendReminder bool + Frequency time.Duration } -func NewNotifierBase(id int64, isDefault bool, name, notifierType string, notifyOnce bool, frequency time.Duration, model *simplejson.Json) NotifierBase { +func NewNotifierBase(model *models.AlertNotification) NotifierBase { uploadImage := true - value, exist := model.CheckGet("uploadImage") + value, exist := model.Settings.CheckGet("uploadImage") if exist { uploadImage = value.MustBool() } return NotifierBase{ - Id: id, - Name: name, - IsDeault: isDefault, - Type: notifierType, - UploadImage: uploadImage, - NotifyOnce: notifyOnce, - Frequency: frequency, + Id: model.Id, + Name: model.Name, + IsDeault: model.IsDefault, + Type: model.Type, + UploadImage: uploadImage, + SendReminder: model.SendReminder, + Frequency: model.Frequency, } } -func defaultShouldNotify(context *alerting.EvalContext, notifyOnce bool, frequency time.Duration, lastNotify *time.Time) bool { +func defaultShouldNotify(context *alerting.EvalContext, sendReminder bool, frequency time.Duration, lastNotify *time.Time) bool { // Only notify on state change. - if context.PrevAlertState == context.Rule.State && notifyOnce { + if context.PrevAlertState == context.Rule.State && !sendReminder { return false } + // Do not notify if interval has not elapsed - if !notifyOnce && lastNotify != nil && lastNotify.Add(frequency).After(time.Now()) { + if sendReminder && lastNotify != nil && lastNotify.Add(frequency).After(time.Now()) { return false } + // Do not notify if alert state if OK or pending even on repeated notify - if !notifyOnce && (context.Rule.State == m.AlertStateOK || context.Rule.State == m.AlertStatePending) { + if sendReminder && (context.Rule.State == models.AlertStateOK || context.Rule.State == models.AlertStatePending) { return false } + // Do not notify when we become OK for the first time. - if (context.PrevAlertState == m.AlertStatePending) && (context.Rule.State == m.AlertStateOK) { + if (context.PrevAlertState == models.AlertStatePending) && (context.Rule.State == models.AlertStateOK) { return false } return true @@ -58,7 +60,7 @@ func defaultShouldNotify(context *alerting.EvalContext, notifyOnce bool, frequen func (n *NotifierBase) ShouldNotify(context *alerting.EvalContext) bool { lastNotify := context.LastNotify(n.Id) - return defaultShouldNotify(context, n.NotifyOnce, n.Frequency, lastNotify) + return defaultShouldNotify(context, n.SendReminder, n.Frequency, lastNotify) } func (n *NotifierBase) GetType() string { @@ -77,8 +79,8 @@ func (n *NotifierBase) GetIsDefault() bool { return n.IsDeault } -func (n *NotifierBase) GetNotifyOnce() bool { - return n.NotifyOnce +func (n *NotifierBase) GetSendReminder() bool { + return n.SendReminder } func (n *NotifierBase) GetFrequency() time.Duration { diff --git a/pkg/services/alerting/notifiers/base_test.go b/pkg/services/alerting/notifiers/base_test.go index 5f2d4989063..28e2ff9f024 100644 --- a/pkg/services/alerting/notifiers/base_test.go +++ b/pkg/services/alerting/notifiers/base_test.go @@ -16,22 +16,29 @@ func TestBaseNotifier(t *testing.T) { Convey("default constructor for notifiers", func() { bJson := simplejson.New() + model := &m.AlertNotification{ + Id: 1, + Name: "name", + Type: "email", + Settings: bJson, + } + Convey("can parse false value", func() { bJson.Set("uploadImage", false) - base := NewNotifierBase(1, false, "name", "email", true, 0, bJson) + base := NewNotifierBase(model) So(base.UploadImage, ShouldBeFalse) }) Convey("can parse true value", func() { bJson.Set("uploadImage", true) - base := NewNotifierBase(1, false, "name", "email", true, 0, bJson) + base := NewNotifierBase(model) So(base.UploadImage, ShouldBeTrue) }) Convey("default value should be true for backwards compatibility", func() { - base := NewNotifierBase(1, false, "name", "email", true, 0, bJson) + base := NewNotifierBase(model) So(base.UploadImage, ShouldBeTrue) }) }) diff --git a/pkg/services/alerting/notifiers/dingding.go b/pkg/services/alerting/notifiers/dingding.go index 78446c56f88..738e43af2d2 100644 --- a/pkg/services/alerting/notifiers/dingding.go +++ b/pkg/services/alerting/notifiers/dingding.go @@ -32,7 +32,7 @@ func NewDingDingNotifier(model *m.AlertNotification) (alerting.Notifier, error) } return &DingDingNotifier{ - NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.NotifyOnce, model.Frequency, model.Settings), + NotifierBase: NewNotifierBase(model), Url: url, log: log.New("alerting.notifier.dingding"), }, nil diff --git a/pkg/services/alerting/notifiers/discord.go b/pkg/services/alerting/notifiers/discord.go index 693ed31e206..57d9d438fa2 100644 --- a/pkg/services/alerting/notifiers/discord.go +++ b/pkg/services/alerting/notifiers/discord.go @@ -39,7 +39,7 @@ func NewDiscordNotifier(model *m.AlertNotification) (alerting.Notifier, error) { } return &DiscordNotifier{ - NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.NotifyOnce, model.Frequency, model.Settings), + NotifierBase: NewNotifierBase(model), WebhookURL: url, log: log.New("alerting.notifier.discord"), }, nil diff --git a/pkg/services/alerting/notifiers/email.go b/pkg/services/alerting/notifiers/email.go index 234a4f8e756..17b88f7d97f 100644 --- a/pkg/services/alerting/notifiers/email.go +++ b/pkg/services/alerting/notifiers/email.go @@ -52,7 +52,7 @@ func NewEmailNotifier(model *m.AlertNotification) (alerting.Notifier, error) { }) return &EmailNotifier{ - NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.NotifyOnce, model.Frequency, model.Settings), + NotifierBase: NewNotifierBase(model), Addresses: addresses, log: log.New("alerting.notifier.email"), }, nil diff --git a/pkg/services/alerting/notifiers/hipchat.go b/pkg/services/alerting/notifiers/hipchat.go index 4eb5b78811e..1c284ec3d2b 100644 --- a/pkg/services/alerting/notifiers/hipchat.go +++ b/pkg/services/alerting/notifiers/hipchat.go @@ -59,7 +59,7 @@ func NewHipChatNotifier(model *models.AlertNotification) (alerting.Notifier, err roomId := model.Settings.Get("roomid").MustString() return &HipChatNotifier{ - NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.NotifyOnce, model.Frequency, model.Settings), + NotifierBase: NewNotifierBase(model), Url: url, ApiKey: apikey, RoomId: roomId, diff --git a/pkg/services/alerting/notifiers/kafka.go b/pkg/services/alerting/notifiers/kafka.go index 0dab556d5e1..d8d19fc5dae 100644 --- a/pkg/services/alerting/notifiers/kafka.go +++ b/pkg/services/alerting/notifiers/kafka.go @@ -43,7 +43,7 @@ func NewKafkaNotifier(model *m.AlertNotification) (alerting.Notifier, error) { } return &KafkaNotifier{ - NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.NotifyOnce, model.Frequency, model.Settings), + NotifierBase: NewNotifierBase(model), Endpoint: endpoint, Topic: topic, log: log.New("alerting.notifier.kafka"), diff --git a/pkg/services/alerting/notifiers/line.go b/pkg/services/alerting/notifiers/line.go index 0ee252e6447..9e3888b8f95 100644 --- a/pkg/services/alerting/notifiers/line.go +++ b/pkg/services/alerting/notifiers/line.go @@ -39,7 +39,7 @@ func NewLINENotifier(model *m.AlertNotification) (alerting.Notifier, error) { } return &LineNotifier{ - NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.NotifyOnce, model.Frequency, model.Settings), + NotifierBase: NewNotifierBase(model), Token: token, log: log.New("alerting.notifier.line"), }, nil diff --git a/pkg/services/alerting/notifiers/opsgenie.go b/pkg/services/alerting/notifiers/opsgenie.go index 991afd5ce9b..84148a0d99c 100644 --- a/pkg/services/alerting/notifiers/opsgenie.go +++ b/pkg/services/alerting/notifiers/opsgenie.go @@ -56,7 +56,7 @@ func NewOpsGenieNotifier(model *m.AlertNotification) (alerting.Notifier, error) } return &OpsGenieNotifier{ - NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.NotifyOnce, model.Frequency, model.Settings), + NotifierBase: NewNotifierBase(model), ApiKey: apiKey, ApiUrl: apiUrl, AutoClose: autoClose, diff --git a/pkg/services/alerting/notifiers/pagerduty.go b/pkg/services/alerting/notifiers/pagerduty.go index afa0ba63eca..bf85466388f 100644 --- a/pkg/services/alerting/notifiers/pagerduty.go +++ b/pkg/services/alerting/notifiers/pagerduty.go @@ -51,7 +51,7 @@ func NewPagerdutyNotifier(model *m.AlertNotification) (alerting.Notifier, error) } return &PagerdutyNotifier{ - NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.NotifyOnce, model.Frequency, model.Settings), + NotifierBase: NewNotifierBase(model), Key: key, AutoResolve: autoResolve, log: log.New("alerting.notifier.pagerduty"), diff --git a/pkg/services/alerting/notifiers/pushover.go b/pkg/services/alerting/notifiers/pushover.go index 09dfd6f0f9b..55dc02c5f4a 100644 --- a/pkg/services/alerting/notifiers/pushover.go +++ b/pkg/services/alerting/notifiers/pushover.go @@ -99,7 +99,7 @@ func NewPushoverNotifier(model *m.AlertNotification) (alerting.Notifier, error) return nil, alerting.ValidationError{Reason: "API token not given"} } return &PushoverNotifier{ - NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.NotifyOnce, model.Frequency, model.Settings), + NotifierBase: NewNotifierBase(model), UserKey: userKey, ApiToken: apiToken, Priority: priority, diff --git a/pkg/services/alerting/notifiers/sensu.go b/pkg/services/alerting/notifiers/sensu.go index e6b94d3223e..21d5d3d9d9e 100644 --- a/pkg/services/alerting/notifiers/sensu.go +++ b/pkg/services/alerting/notifiers/sensu.go @@ -51,7 +51,7 @@ func NewSensuNotifier(model *m.AlertNotification) (alerting.Notifier, error) { } return &SensuNotifier{ - NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.NotifyOnce, model.Frequency, model.Settings), + NotifierBase: NewNotifierBase(model), Url: url, User: model.Settings.Get("username").MustString(), Source: model.Settings.Get("source").MustString(), diff --git a/pkg/services/alerting/notifiers/slack.go b/pkg/services/alerting/notifiers/slack.go index fbbe4b3e59d..93c9a0accc0 100644 --- a/pkg/services/alerting/notifiers/slack.go +++ b/pkg/services/alerting/notifiers/slack.go @@ -78,7 +78,7 @@ func NewSlackNotifier(model *m.AlertNotification) (alerting.Notifier, error) { uploadImage := model.Settings.Get("uploadImage").MustBool(true) return &SlackNotifier{ - NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.NotifyOnce, model.Frequency, model.Settings), + NotifierBase: NewNotifierBase(model), Url: url, Recipient: recipient, Mention: mention, diff --git a/pkg/services/alerting/notifiers/teams.go b/pkg/services/alerting/notifiers/teams.go index 362a367e1f2..58dd4b22bb7 100644 --- a/pkg/services/alerting/notifiers/teams.go +++ b/pkg/services/alerting/notifiers/teams.go @@ -33,7 +33,7 @@ func NewTeamsNotifier(model *m.AlertNotification) (alerting.Notifier, error) { } return &TeamsNotifier{ - NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.NotifyOnce, model.Frequency, model.Settings), + NotifierBase: NewNotifierBase(model), Url: url, log: log.New("alerting.notifier.teams"), }, nil diff --git a/pkg/services/alerting/notifiers/telegram.go b/pkg/services/alerting/notifiers/telegram.go index 97696b2290c..b03f7ca38c5 100644 --- a/pkg/services/alerting/notifiers/telegram.go +++ b/pkg/services/alerting/notifiers/telegram.go @@ -78,7 +78,7 @@ func NewTelegramNotifier(model *m.AlertNotification) (alerting.Notifier, error) } return &TelegramNotifier{ - NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.NotifyOnce, model.Frequency, model.Settings), + NotifierBase: NewNotifierBase(model), BotToken: botToken, ChatID: chatId, UploadImage: uploadImage, diff --git a/pkg/services/alerting/notifiers/threema.go b/pkg/services/alerting/notifiers/threema.go index e7fb39f27db..28a62fade17 100644 --- a/pkg/services/alerting/notifiers/threema.go +++ b/pkg/services/alerting/notifiers/threema.go @@ -106,7 +106,7 @@ func NewThreemaNotifier(model *m.AlertNotification) (alerting.Notifier, error) { } return &ThreemaNotifier{ - NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.NotifyOnce, model.Frequency, model.Settings), + NotifierBase: NewNotifierBase(model), GatewayID: gatewayID, RecipientID: recipientID, APISecret: apiSecret, diff --git a/pkg/services/alerting/notifiers/victorops.go b/pkg/services/alerting/notifiers/victorops.go index c6c1cf76047..3093aec9957 100644 --- a/pkg/services/alerting/notifiers/victorops.go +++ b/pkg/services/alerting/notifiers/victorops.go @@ -51,7 +51,7 @@ func NewVictoropsNotifier(model *models.AlertNotification) (alerting.Notifier, e } return &VictoropsNotifier{ - NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.NotifyOnce, model.Frequency, model.Settings), + NotifierBase: NewNotifierBase(model), URL: url, AutoResolve: autoResolve, log: log.New("alerting.notifier.victorops"), diff --git a/pkg/services/alerting/notifiers/webhook.go b/pkg/services/alerting/notifiers/webhook.go index 26989873e9e..4045e496af9 100644 --- a/pkg/services/alerting/notifiers/webhook.go +++ b/pkg/services/alerting/notifiers/webhook.go @@ -47,7 +47,7 @@ func NewWebHookNotifier(model *m.AlertNotification) (alerting.Notifier, error) { } return &WebhookNotifier{ - NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.NotifyOnce, model.Frequency, model.Settings), + NotifierBase: NewNotifierBase(model), Url: url, User: model.Settings.Get("username").MustString(), Password: model.Settings.Get("password").MustString(), diff --git a/pkg/services/sqlstore/alert_notification.go b/pkg/services/sqlstore/alert_notification.go index ff36c38b1a5..8c136bd3887 100644 --- a/pkg/services/sqlstore/alert_notification.go +++ b/pkg/services/sqlstore/alert_notification.go @@ -57,7 +57,7 @@ func GetAlertNotificationsToSend(query *m.GetAlertNotificationsToSendQuery) erro alert_notification.updated, alert_notification.settings, alert_notification.is_default, - alert_notification.notify_once, + alert_notification.send_reminder, alert_notification.frequency FROM alert_notification `) @@ -97,7 +97,7 @@ func getAlertNotificationInternal(query *m.GetAlertNotificationsQuery, sess *DBS alert_notification.updated, alert_notification.settings, alert_notification.is_default, - alert_notification.notify_once, + alert_notification.send_reminder, alert_notification.frequency FROM alert_notification `) @@ -145,7 +145,7 @@ func CreateAlertNotificationCommand(cmd *m.CreateAlertNotificationCommand) error } var frequency time.Duration - if !cmd.NotifyOnce { + if cmd.SendReminder { if cmd.Frequency == "" { return m.ErrNotificationFrequencyNotFound } @@ -157,18 +157,18 @@ func CreateAlertNotificationCommand(cmd *m.CreateAlertNotificationCommand) error } alertNotification := &m.AlertNotification{ - OrgId: cmd.OrgId, - Name: cmd.Name, - Type: cmd.Type, - Settings: cmd.Settings, - NotifyOnce: cmd.NotifyOnce, - Frequency: frequency, - Created: time.Now(), - Updated: time.Now(), - IsDefault: cmd.IsDefault, + OrgId: cmd.OrgId, + Name: cmd.Name, + Type: cmd.Type, + Settings: cmd.Settings, + SendReminder: cmd.SendReminder, + Frequency: frequency, + Created: time.Now(), + Updated: time.Now(), + IsDefault: cmd.IsDefault, } - if _, err = sess.MustCols("notify_once").Insert(alertNotification); err != nil { + if _, err = sess.MustCols("send_reminder").Insert(alertNotification); err != nil { return err } @@ -200,9 +200,9 @@ func UpdateAlertNotification(cmd *m.UpdateAlertNotificationCommand) error { current.Name = cmd.Name current.Type = cmd.Type current.IsDefault = cmd.IsDefault - current.NotifyOnce = cmd.NotifyOnce + current.SendReminder = cmd.SendReminder - if !current.NotifyOnce { + if current.SendReminder { if cmd.Frequency == "" { return m.ErrNotificationFrequencyNotFound } @@ -215,7 +215,7 @@ func UpdateAlertNotification(cmd *m.UpdateAlertNotificationCommand) error { current.Frequency = frequency } - sess.UseBool("is_default", "notify_once") + sess.UseBool("is_default", "send_reminder") if affected, err := sess.ID(cmd.Id).Update(current); err != nil { return err diff --git a/pkg/services/sqlstore/alert_notification_test.go b/pkg/services/sqlstore/alert_notification_test.go index 578a53f34ad..fe7f02b22b0 100644 --- a/pkg/services/sqlstore/alert_notification_test.go +++ b/pkg/services/sqlstore/alert_notification_test.go @@ -23,13 +23,13 @@ func TestAlertNotificationSQLAccess(t *testing.T) { So(cmd.Result, ShouldBeNil) }) - Convey("Cannot save alert notifier with notitfyonce = false", func() { + Convey("Cannot save alert notifier with send reminder = true", func() { cmd := &m.CreateAlertNotificationCommand{ - Name: "ops", - Type: "email", - OrgId: 1, - NotifyOnce: false, - Settings: simplejson.New(), + Name: "ops", + Type: "email", + OrgId: 1, + SendReminder: true, + Settings: simplejson.New(), } Convey("and missing frequency", func() { @@ -47,19 +47,19 @@ func TestAlertNotificationSQLAccess(t *testing.T) { Convey("Cannot update alert notifier with notitfyonce = false", func() { cmd := &m.CreateAlertNotificationCommand{ - Name: "ops update", - Type: "email", - OrgId: 1, - NotifyOnce: true, - Settings: simplejson.New(), + Name: "ops update", + Type: "email", + OrgId: 1, + SendReminder: false, + Settings: simplejson.New(), } err := CreateAlertNotificationCommand(cmd) So(err, ShouldBeNil) updateCmd := &m.UpdateAlertNotificationCommand{ - Id: cmd.Result.Id, - NotifyOnce: false, + Id: cmd.Result.Id, + SendReminder: true, } Convey("and missing frequency", func() { @@ -71,18 +71,19 @@ func TestAlertNotificationSQLAccess(t *testing.T) { updateCmd.Frequency = "invalid duration" err := UpdateAlertNotification(updateCmd) + So(err, ShouldNotBeNil) So(err.Error(), ShouldEqual, "time: invalid duration invalid duration") }) }) Convey("Can save Alert Notification", func() { cmd := &m.CreateAlertNotificationCommand{ - Name: "ops", - Type: "email", - OrgId: 1, - NotifyOnce: true, - Frequency: "10s", - Settings: simplejson.New(), + Name: "ops", + Type: "email", + OrgId: 1, + SendReminder: true, + Frequency: "10s", + Settings: simplejson.New(), } err := CreateAlertNotificationCommand(cmd) @@ -98,13 +99,13 @@ func TestAlertNotificationSQLAccess(t *testing.T) { Convey("Can update alert notification", func() { newCmd := &m.UpdateAlertNotificationCommand{ - Name: "NewName", - Type: "webhook", - OrgId: cmd.Result.OrgId, - NotifyOnce: true, - Frequency: "10s", - Settings: simplejson.New(), - Id: cmd.Result.Id, + Name: "NewName", + Type: "webhook", + OrgId: cmd.Result.OrgId, + SendReminder: true, + Frequency: "10s", + Settings: simplejson.New(), + Id: cmd.Result.Id, } err := UpdateAlertNotification(newCmd) So(err, ShouldBeNil) @@ -113,12 +114,12 @@ func TestAlertNotificationSQLAccess(t *testing.T) { }) Convey("Can search using an array of ids", func() { - cmd1 := m.CreateAlertNotificationCommand{Name: "nagios", Type: "webhook", OrgId: 1, NotifyOnce: true, Frequency: "10s", Settings: simplejson.New()} - cmd2 := m.CreateAlertNotificationCommand{Name: "slack", Type: "webhook", OrgId: 1, NotifyOnce: true, Frequency: "10s", Settings: simplejson.New()} - cmd3 := m.CreateAlertNotificationCommand{Name: "ops2", Type: "email", OrgId: 1, NotifyOnce: true, Frequency: "10s", Settings: simplejson.New()} - cmd4 := m.CreateAlertNotificationCommand{IsDefault: true, Name: "default", Type: "email", OrgId: 1, NotifyOnce: true, Frequency: "10s", Settings: simplejson.New()} + cmd1 := m.CreateAlertNotificationCommand{Name: "nagios", Type: "webhook", OrgId: 1, SendReminder: true, Frequency: "10s", Settings: simplejson.New()} + cmd2 := m.CreateAlertNotificationCommand{Name: "slack", Type: "webhook", OrgId: 1, SendReminder: true, Frequency: "10s", Settings: simplejson.New()} + cmd3 := m.CreateAlertNotificationCommand{Name: "ops2", Type: "email", OrgId: 1, SendReminder: true, Frequency: "10s", Settings: simplejson.New()} + cmd4 := m.CreateAlertNotificationCommand{IsDefault: true, Name: "default", Type: "email", OrgId: 1, SendReminder: true, Frequency: "10s", Settings: simplejson.New()} - otherOrg := m.CreateAlertNotificationCommand{Name: "default", Type: "email", OrgId: 2, NotifyOnce: true, Frequency: "10s", Settings: simplejson.New()} + otherOrg := m.CreateAlertNotificationCommand{Name: "default", Type: "email", OrgId: 2, SendReminder: true, Frequency: "10s", Settings: simplejson.New()} So(CreateAlertNotificationCommand(&cmd1), ShouldBeNil) So(CreateAlertNotificationCommand(&cmd2), ShouldBeNil) diff --git a/pkg/services/sqlstore/migrations/alert_mig.go b/pkg/services/sqlstore/migrations/alert_mig.go index d045f611fb2..51509099c7d 100644 --- a/pkg/services/sqlstore/migrations/alert_mig.go +++ b/pkg/services/sqlstore/migrations/alert_mig.go @@ -68,9 +68,10 @@ func addAlertMigrations(mg *Migrator) { mg.AddMigration("Add column frequency", NewAddColumnMigration(alert_notification, &Column{ Name: "frequency", Type: DB_BigInt, Nullable: true, })) - mg.AddMigration("Add column notify_once", NewAddColumnMigration(alert_notification, &Column{ - Name: "notify_once", Type: DB_Bool, Nullable: false, Default: "1", + mg.AddMigration("Add column send_reminder", NewAddColumnMigration(alert_notification, &Column{ + Name: "send_reminder", Type: DB_Bool, Nullable: true, Default: "0", })) + mg.AddMigration("add index alert_notification org_id & name", NewAddIndexMigration(alert_notification, alert_notification.Indices[0])) notification_journal := Table{ diff --git a/public/app/features/alerting/notification_edit_ctrl.ts b/public/app/features/alerting/notification_edit_ctrl.ts index 9d20e871c7c..e066406bc43 100644 --- a/public/app/features/alerting/notification_edit_ctrl.ts +++ b/public/app/features/alerting/notification_edit_ctrl.ts @@ -11,7 +11,7 @@ export class AlertNotificationEditCtrl { model: any; defaults: any = { type: 'email', - notifyOnce: true, + sendReminder: false, frequency: '15m', settings: { httpMethod: 'POST', diff --git a/public/app/features/alerting/partials/notification_edit.html b/public/app/features/alerting/partials/notification_edit.html index 48d44b74581..2a2fbb131b8 100644 --- a/public/app/features/alerting/partials/notification_edit.html +++ b/public/app/features/alerting/partials/notification_edit.html @@ -34,14 +34,27 @@ -
- Notify every - +
+
+ Send reminder every + + + Specify at what interval you want reminder's about this alerting beeing triggered. + Ex. 60s, 10m, 30m, 1h + +
From bcbae7aa6240e974d47f6c55d83195e4ef2348ad Mon Sep 17 00:00:00 2001 From: bergquist Date: Tue, 5 Jun 2018 12:07:02 +0200 Subject: [PATCH 061/786] alerting: move queries from evalcontext to notifier base --- pkg/services/alerting/eval_context.go | 15 --------------- pkg/services/alerting/interfaces.go | 2 ++ pkg/services/alerting/notifier.go | 1 + pkg/services/alerting/notifiers/base.go | 23 ++++++++++++++++++++--- 4 files changed, 23 insertions(+), 18 deletions(-) diff --git a/pkg/services/alerting/eval_context.go b/pkg/services/alerting/eval_context.go index 3817f4b4a3c..d0441d379b7 100644 --- a/pkg/services/alerting/eval_context.go +++ b/pkg/services/alerting/eval_context.go @@ -143,18 +143,3 @@ func (c *EvalContext) GetNewState() m.AlertStateType { return m.AlertStateOK } - -func (c *EvalContext) LastNotify(notifierId int64) *time.Time { - cmd := &m.GetLatestNotificationQuery{ - OrgId: c.Rule.OrgId, - AlertId: c.Rule.Id, - NotifierId: notifierId, - } - if err := bus.Dispatch(cmd); err != nil { - c.log.Warn("Could not determine last time alert notifier fired", - "Alert name", c.Rule.Name, "Error", err) - return nil - } - - return &cmd.Result.SentAt -} diff --git a/pkg/services/alerting/interfaces.go b/pkg/services/alerting/interfaces.go index 95fd4b5d04e..b4376191df0 100644 --- a/pkg/services/alerting/interfaces.go +++ b/pkg/services/alerting/interfaces.go @@ -15,6 +15,8 @@ type Notifier interface { Notify(evalContext *EvalContext) error GetType() string NeedsImage() bool + + // ShouldNotify checks this evaluation should send an alert notification ShouldNotify(evalContext *EvalContext) bool GetNotifierId() int64 diff --git a/pkg/services/alerting/notifier.go b/pkg/services/alerting/notifier.go index 53923a420fe..363a156c5ec 100644 --- a/pkg/services/alerting/notifier.go +++ b/pkg/services/alerting/notifier.go @@ -131,6 +131,7 @@ func (n *notificationService) getNeededNotifiers(orgId int64, notificationIds [] if err != nil { return nil, err } + if not.ShouldNotify(context) { result = append(result, not) } diff --git a/pkg/services/alerting/notifiers/base.go b/pkg/services/alerting/notifiers/base.go index 1d0d904457f..fc00bd5265e 100644 --- a/pkg/services/alerting/notifiers/base.go +++ b/pkg/services/alerting/notifiers/base.go @@ -3,6 +3,8 @@ package notifiers import ( "time" + "github.com/grafana/grafana/pkg/bus" + "github.com/grafana/grafana/pkg/log" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/alerting" ) @@ -15,6 +17,8 @@ type NotifierBase struct { UploadImage bool SendReminder bool Frequency time.Duration + + log log.Logger } func NewNotifierBase(model *models.AlertNotification) NotifierBase { @@ -32,6 +36,7 @@ func NewNotifierBase(model *models.AlertNotification) NotifierBase { UploadImage: uploadImage, SendReminder: model.SendReminder, Frequency: model.Frequency, + log: log.New("alerting.notifier." + model.Name), } } @@ -55,12 +60,24 @@ func defaultShouldNotify(context *alerting.EvalContext, sendReminder bool, frequ if (context.PrevAlertState == models.AlertStatePending) && (context.Rule.State == models.AlertStateOK) { return false } + return true } -func (n *NotifierBase) ShouldNotify(context *alerting.EvalContext) bool { - lastNotify := context.LastNotify(n.Id) - return defaultShouldNotify(context, n.SendReminder, n.Frequency, lastNotify) +// ShouldNotify checks this evaluation should send an alert notification +func (n *NotifierBase) ShouldNotify(c *alerting.EvalContext) bool { + cmd := &models.GetLatestNotificationQuery{ + OrgId: c.Rule.OrgId, + AlertId: c.Rule.Id, + NotifierId: n.Id, + } + + if err := bus.Dispatch(cmd); err != nil { + n.log.Error("Could not determine last time alert notifier fired", "Alert name", c.Rule.Name, "Error", err) + return false + } + + return defaultShouldNotify(c, n.SendReminder, n.Frequency, &cmd.Result.SentAt) } func (n *NotifierBase) GetType() string { From ab70ead5e4d7ab1dabb204e02a4e27e453d6bd67 Mon Sep 17 00:00:00 2001 From: bergquist Date: Tue, 5 Jun 2018 13:10:38 +0200 Subject: [PATCH 062/786] alerting: renames journal table to alert_notification_journal --- pkg/services/sqlstore/migrations/alert_mig.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/services/sqlstore/migrations/alert_mig.go b/pkg/services/sqlstore/migrations/alert_mig.go index 51509099c7d..e58959929d1 100644 --- a/pkg/services/sqlstore/migrations/alert_mig.go +++ b/pkg/services/sqlstore/migrations/alert_mig.go @@ -75,7 +75,7 @@ func addAlertMigrations(mg *Migrator) { mg.AddMigration("add index alert_notification org_id & name", NewAddIndexMigration(alert_notification, alert_notification.Indices[0])) notification_journal := Table{ - Name: "notification_journal", + Name: "alert_notification_journal", Columns: []*Column{ {Name: "id", Type: DB_BigInt, IsPrimaryKey: true, IsAutoIncrement: true}, {Name: "org_id", Type: DB_BigInt, Nullable: false}, From 171a38df999f919d2bbd59344483759fc83c7af0 Mon Sep 17 00:00:00 2001 From: bergquist Date: Tue, 5 Jun 2018 14:29:48 +0200 Subject: [PATCH 063/786] alerting: fixes broken table rename --- pkg/models/alert_notifications.go | 4 +-- pkg/services/sqlstore/alert_notification.go | 40 ++++++++++++++++++--- 2 files changed, 38 insertions(+), 6 deletions(-) diff --git a/pkg/models/alert_notifications.go b/pkg/models/alert_notifications.go index c17124dd6ef..8df7a830d8b 100644 --- a/pkg/models/alert_notifications.go +++ b/pkg/models/alert_notifications.go @@ -75,7 +75,7 @@ type GetAllAlertNotificationsQuery struct { Result []*AlertNotification } -type NotificationJournal struct { +type AlertNotificationJournal struct { Id int64 OrgId int64 AlertId int64 @@ -97,7 +97,7 @@ type GetLatestNotificationQuery struct { AlertId int64 NotifierId int64 - Result *NotificationJournal + Result *AlertNotificationJournal } type CleanNotificationJournalCommand struct { diff --git a/pkg/services/sqlstore/alert_notification.go b/pkg/services/sqlstore/alert_notification.go index 8c136bd3887..0223636bd9a 100644 --- a/pkg/services/sqlstore/alert_notification.go +++ b/pkg/services/sqlstore/alert_notification.go @@ -2,10 +2,12 @@ package sqlstore import ( "bytes" + "context" "fmt" "strings" "time" + "github.com/go-xorm/xorm" "github.com/grafana/grafana/pkg/bus" m "github.com/grafana/grafana/pkg/models" ) @@ -20,6 +22,8 @@ func init() { bus.AddHandler("sql", RecordNotificationJournal) bus.AddHandler("sql", GetLatestNotification) bus.AddHandler("sql", CleanNotificationJournal) + + bus.AddCtxHandler("sql", GetLastestNotification2) } func DeleteAlertNotification(cmd *m.DeleteAlertNotificationCommand) error { @@ -230,7 +234,7 @@ func UpdateAlertNotification(cmd *m.UpdateAlertNotificationCommand) error { func RecordNotificationJournal(cmd *m.RecordNotificationJournalCommand) error { return inTransaction(func(sess *DBSession) error { - journalEntry := &m.NotificationJournal{ + journalEntry := &m.AlertNotificationJournal{ OrgId: cmd.OrgId, AlertId: cmd.AlertId, NotifierId: cmd.NotifierId, @@ -246,10 +250,38 @@ func RecordNotificationJournal(cmd *m.RecordNotificationJournalCommand) error { }) } +func startSession(ctx context.Context) *DBSession { + value := ctx.Value("db-session") + var sess *xorm.Session + sess, ok := value.(*xorm.Session) + + if !ok { + return newSession() + } + + old := newSession() + old.Session = sess + + return old +} + +func GetLastestNotification2(ctx context.Context, cmd *m.GetLatestNotificationQuery) error { + sess := startSession(ctx) + + notificationJournal := &m.AlertNotificationJournal{} + _, err := sess.Desc("alert_notification_journal.sent_at").Limit(1).Where("alert_notification_journal.org_id = ? AND alert_notification_journal.alert_id = ? AND alert_notification_journal.notifier_id = ?", cmd.OrgId, cmd.AlertId, cmd.NotifierId).Get(notificationJournal) + if err != nil { + return err + } + + cmd.Result = notificationJournal + return nil +} + func GetLatestNotification(cmd *m.GetLatestNotificationQuery) error { return inTransaction(func(sess *DBSession) error { - notificationJournal := &m.NotificationJournal{} - _, err := sess.Desc("notification_journal.sent_at").Limit(1).Where("notification_journal.org_id = ? AND notification_journal.alert_id = ? AND notification_journal.notifier_id = ?", cmd.OrgId, cmd.AlertId, cmd.NotifierId).Get(notificationJournal) + notificationJournal := &m.AlertNotificationJournal{} + _, err := sess.Desc("alert_notification_journal.sent_at").Limit(1).Where("alert_notification_journal.org_id = ? AND alert_notification_journal.alert_id = ? AND alert_notification_journal.notifier_id = ?", cmd.OrgId, cmd.AlertId, cmd.NotifierId).Get(notificationJournal) if err != nil { return err } @@ -261,7 +293,7 @@ func GetLatestNotification(cmd *m.GetLatestNotificationQuery) error { func CleanNotificationJournal(cmd *m.CleanNotificationJournalCommand) error { return inTransaction(func(sess *DBSession) error { - sql := "DELETE FROM notification_journal WHERE notification_journal.org_id = ? AND notification_journal.alert_id = ? AND notification_journal.notifier_id = ?" + sql := "DELETE FROM alert_notification_journal WHERE notification_journal.org_id = ? AND alert_notification_journal.alert_id = ? AND alert_notification_journal.notifier_id = ?" _, err := sess.Exec(sql, cmd.OrgId, cmd.AlertId, cmd.NotifierId) return err }) From 850aa21d451618590a8530b02d4ffd68e02bbeb6 Mon Sep 17 00:00:00 2001 From: bergquist Date: Thu, 7 Jun 2018 07:06:13 +0200 Subject: [PATCH 064/786] removes unused code --- pkg/services/sqlstore/alert_notification.go | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/pkg/services/sqlstore/alert_notification.go b/pkg/services/sqlstore/alert_notification.go index 0223636bd9a..a2f3e629ae6 100644 --- a/pkg/services/sqlstore/alert_notification.go +++ b/pkg/services/sqlstore/alert_notification.go @@ -22,8 +22,6 @@ func init() { bus.AddHandler("sql", RecordNotificationJournal) bus.AddHandler("sql", GetLatestNotification) bus.AddHandler("sql", CleanNotificationJournal) - - bus.AddCtxHandler("sql", GetLastestNotification2) } func DeleteAlertNotification(cmd *m.DeleteAlertNotificationCommand) error { @@ -265,19 +263,6 @@ func startSession(ctx context.Context) *DBSession { return old } -func GetLastestNotification2(ctx context.Context, cmd *m.GetLatestNotificationQuery) error { - sess := startSession(ctx) - - notificationJournal := &m.AlertNotificationJournal{} - _, err := sess.Desc("alert_notification_journal.sent_at").Limit(1).Where("alert_notification_journal.org_id = ? AND alert_notification_journal.alert_id = ? AND alert_notification_journal.notifier_id = ?", cmd.OrgId, cmd.AlertId, cmd.NotifierId).Get(notificationJournal) - if err != nil { - return err - } - - cmd.Result = notificationJournal - return nil -} - func GetLatestNotification(cmd *m.GetLatestNotificationQuery) error { return inTransaction(func(sess *DBSession) error { notificationJournal := &m.AlertNotificationJournal{} From 4a8e9cf93f9a04a30be8269ac68653889394a53d Mon Sep 17 00:00:00 2001 From: bergquist Date: Thu, 7 Jun 2018 07:15:50 +0200 Subject: [PATCH 065/786] removes more unused code --- pkg/services/sqlstore/alert_notification.go | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/pkg/services/sqlstore/alert_notification.go b/pkg/services/sqlstore/alert_notification.go index a2f3e629ae6..167b9b3bd61 100644 --- a/pkg/services/sqlstore/alert_notification.go +++ b/pkg/services/sqlstore/alert_notification.go @@ -2,12 +2,10 @@ package sqlstore import ( "bytes" - "context" "fmt" "strings" "time" - "github.com/go-xorm/xorm" "github.com/grafana/grafana/pkg/bus" m "github.com/grafana/grafana/pkg/models" ) @@ -248,21 +246,6 @@ func RecordNotificationJournal(cmd *m.RecordNotificationJournalCommand) error { }) } -func startSession(ctx context.Context) *DBSession { - value := ctx.Value("db-session") - var sess *xorm.Session - sess, ok := value.(*xorm.Session) - - if !ok { - return newSession() - } - - old := newSession() - old.Session = sess - - return old -} - func GetLatestNotification(cmd *m.GetLatestNotificationQuery) error { return inTransaction(func(sess *DBSession) error { notificationJournal := &m.AlertNotificationJournal{} From 23c97d080ff6892379038e3742d712aa41c5b771 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Niemann?= Date: Wed, 13 Jun 2018 09:43:33 +0200 Subject: [PATCH 066/786] added id tag to Panels for html bookmarking on longer Dashboards --- public/app/features/dashboard/dashgrid/DashboardGrid.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/app/features/dashboard/dashgrid/DashboardGrid.tsx b/public/app/features/dashboard/dashgrid/DashboardGrid.tsx index 290e587eace..457ad4ef56c 100644 --- a/public/app/features/dashboard/dashgrid/DashboardGrid.tsx +++ b/public/app/features/dashboard/dashgrid/DashboardGrid.tsx @@ -177,7 +177,7 @@ export class DashboardGrid extends React.Component { for (let panel of this.dashboard.panels) { const panelClasses = classNames({ panel: true, 'panel--fullscreen': panel.fullscreen }); panelElements.push( -
+
); From 7632983c627b9e5f36e9b13455dc6a45031629eb Mon Sep 17 00:00:00 2001 From: bergquist Date: Fri, 8 Jun 2018 15:51:26 +0200 Subject: [PATCH 067/786] notifications: gather actions in one transaction --- pkg/services/alerting/notifier.go | 47 +++++++++++++++++-------- pkg/services/alerting/notifiers/base.go | 6 +++- 2 files changed, 37 insertions(+), 16 deletions(-) diff --git a/pkg/services/alerting/notifier.go b/pkg/services/alerting/notifier.go index 363a156c5ec..a3d016211c3 100644 --- a/pkg/services/alerting/notifier.go +++ b/pkg/services/alerting/notifier.go @@ -1,6 +1,7 @@ package alerting import ( + "context" "errors" "fmt" "time" @@ -59,23 +60,39 @@ func (n *notificationService) SendIfNeeded(context *EvalContext) error { return n.sendNotifications(context, notifiers) } -func (n *notificationService) sendNotifications(context *EvalContext, notifiers []Notifier) error { - g, _ := errgroup.WithContext(context.Ctx) +func (n *notificationService) sendNotifications(evalContext *EvalContext, notifiers []Notifier) error { + g, _ := errgroup.WithContext(evalContext.Ctx) for _, notifier := range notifiers { not := notifier //avoid updating scope variable in go routine - n.log.Debug("Sending notification", "type", not.GetType(), "id", not.GetNotifierId(), "isDefault", not.GetIsDefault()) - metrics.M_Alerting_Notification_Sent.WithLabelValues(not.GetType()).Inc() + g.Go(func() error { - success := not.Notify(context) == nil - cmd := &m.RecordNotificationJournalCommand{ - OrgId: context.Rule.OrgId, - AlertId: context.Rule.Id, - NotifierId: not.GetNotifierId(), - SentAt: time.Now(), - Success: success, - } - return bus.Dispatch(cmd) + return bus.InTransaction(evalContext.Ctx, func(ctx context.Context) error { + n.log.Debug("trying to send notification", "id", not.GetNotifierId()) + + // Verify that we can send the notification again + // but this time within the same transaction. + if !evalContext.IsTestRun && !not.ShouldNotify(evalContext) { + return nil + } + + n.log.Debug("Sending notification", "type", not.GetType(), "id", not.GetNotifierId(), "isDefault", not.GetIsDefault()) + metrics.M_Alerting_Notification_Sent.WithLabelValues(not.GetType()).Inc() + + //send notification + success := not.Notify(evalContext) == nil + + //write result to db. + cmd := &m.RecordNotificationJournalCommand{ + OrgId: evalContext.Rule.OrgId, + AlertId: evalContext.Rule.Id, + NotifierId: not.GetNotifierId(), + SentAt: time.Now(), + Success: success, + } + + return bus.DispatchCtx(evalContext.Ctx, cmd) + }) }) } @@ -118,7 +135,7 @@ func (n *notificationService) uploadImage(context *EvalContext) (err error) { return nil } -func (n *notificationService) getNeededNotifiers(orgId int64, notificationIds []int64, context *EvalContext) (NotifierSlice, error) { +func (n *notificationService) getNeededNotifiers(orgId int64, notificationIds []int64, evalContext *EvalContext) (NotifierSlice, error) { query := &m.GetAlertNotificationsToSendQuery{OrgId: orgId, Ids: notificationIds} if err := bus.Dispatch(query); err != nil { @@ -132,7 +149,7 @@ func (n *notificationService) getNeededNotifiers(orgId int64, notificationIds [] return nil, err } - if not.ShouldNotify(context) { + if not.ShouldNotify(evalContext) { result = append(result, not) } } diff --git a/pkg/services/alerting/notifiers/base.go b/pkg/services/alerting/notifiers/base.go index d8cb740daa1..8450816f97e 100644 --- a/pkg/services/alerting/notifiers/base.go +++ b/pkg/services/alerting/notifiers/base.go @@ -73,11 +73,15 @@ func (n *NotifierBase) ShouldNotify(c *alerting.EvalContext) bool { NotifierId: n.Id, } - if err := bus.Dispatch(cmd); err != nil { + if err := bus.DispatchCtx(c.Ctx, cmd); err != nil { n.log.Error("Could not determine last time alert notifier fired", "Alert name", c.Rule.Name, "Error", err) return false } + if !cmd.Result.Success { + return true + } + return defaultShouldNotify(c, n.SendReminder, n.Frequency, &cmd.Result.SentAt) } From acdc2bf100348530d7f8630d78da85434c8be8d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Francisco=20Guimar=C3=A3es?= Date: Fri, 15 Jun 2018 10:11:32 -0300 Subject: [PATCH 068/786] Adding Cloudwatch AWS/AppSync metrics and dimensions --- pkg/tsdb/cloudwatch/metric_find_query.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pkg/tsdb/cloudwatch/metric_find_query.go b/pkg/tsdb/cloudwatch/metric_find_query.go index 136ee241c2e..12c2aba4681 100644 --- a/pkg/tsdb/cloudwatch/metric_find_query.go +++ b/pkg/tsdb/cloudwatch/metric_find_query.go @@ -86,6 +86,7 @@ func init() { "AWS/Kinesis": {"GetRecords.Bytes", "GetRecords.IteratorAge", "GetRecords.IteratorAgeMilliseconds", "GetRecords.Latency", "GetRecords.Records", "GetRecords.Success", "IncomingBytes", "IncomingRecords", "PutRecord.Bytes", "PutRecord.Latency", "PutRecord.Success", "PutRecords.Bytes", "PutRecords.Latency", "PutRecords.Records", "PutRecords.Success", "ReadProvisionedThroughputExceeded", "WriteProvisionedThroughputExceeded", "IteratorAgeMilliseconds", "OutgoingBytes", "OutgoingRecords"}, "AWS/KinesisAnalytics": {"Bytes", "MillisBehindLatest", "Records", "Success"}, "AWS/Lambda": {"Invocations", "Errors", "Duration", "Throttles", "IteratorAge"}, + "AWS/AppSync": {"Latency", "4XXError", "5XXError"}, "AWS/Logs": {"IncomingBytes", "IncomingLogEvents", "ForwardedBytes", "ForwardedLogEvents", "DeliveryErrors", "DeliveryThrottling"}, "AWS/ML": {"PredictCount", "PredictFailureCount"}, "AWS/NATGateway": {"PacketsOutToDestination", "PacketsOutToSource", "PacketsInFromSource", "PacketsInFromDestination", "BytesOutToDestination", "BytesOutToSource", "BytesInFromSource", "BytesInFromDestination", "ErrorPortAllocation", "ActiveConnectionCount", "ConnectionAttemptCount", "ConnectionEstablishedCount", "IdleTimeoutCount", "PacketsDropCount"}, @@ -135,6 +136,7 @@ func init() { "AWS/Kinesis": {"StreamName", "ShardId"}, "AWS/KinesisAnalytics": {"Flow", "Id", "Application"}, "AWS/Lambda": {"FunctionName", "Resource", "Version", "Alias"}, + "AWS/AppSync": {"GraphQLAPIId"}, "AWS/Logs": {"LogGroupName", "DestinationType", "FilterName"}, "AWS/ML": {"MLModelId", "RequestMode"}, "AWS/NATGateway": {"NatGatewayId"}, From f4b089d5519fbd352874282f771c8dc66731dde2 Mon Sep 17 00:00:00 2001 From: bergquist Date: Fri, 15 Jun 2018 15:30:17 +0200 Subject: [PATCH 069/786] notifications: make journaling ctx aware --- pkg/services/alerting/notifiers/base_test.go | 94 +++++++++++--------- pkg/services/alerting/result_handler.go | 2 +- pkg/services/sqlstore/alert_notification.go | 19 ++-- pkg/services/sqlstore/transactions.go | 4 + 4 files changed, 66 insertions(+), 53 deletions(-) diff --git a/pkg/services/alerting/notifiers/base_test.go b/pkg/services/alerting/notifiers/base_test.go index 28e2ff9f024..b7395030e5b 100644 --- a/pkg/services/alerting/notifiers/base_test.go +++ b/pkg/services/alerting/notifiers/base_test.go @@ -11,56 +11,64 @@ import ( . "github.com/smartystreets/goconvey/convey" ) +func TestShouldSendAlertNotification(t *testing.T) { + tcs := []struct { + prevState m.AlertStateType + newState m.AlertStateType + expected bool + }{ + { + newState: m.AlertStatePending, + prevState: m.AlertStateOK, + expected: false, + }, + { + newState: m.AlertStateOK, + prevState: m.AlertStateAlerting, + expected: true, + }, + } + + for _, tc := range tcs { + context := alerting.NewEvalContext(context.TODO(), &alerting.Rule{ + State: tc.newState, + }) + context.Rule.State = tc.prevState + timeNow := time.Now() + if defaultShouldNotify(context, true, 0, &timeNow) != tc.expected { + t.Errorf("expected %v to return %v", tc, tc.expected) + } + } +} + func TestBaseNotifier(t *testing.T) { - Convey("Base notifier tests", t, func() { - Convey("default constructor for notifiers", func() { - bJson := simplejson.New() + Convey("default constructor for notifiers", t, func() { + bJson := simplejson.New() - model := &m.AlertNotification{ - Id: 1, - Name: "name", - Type: "email", - Settings: bJson, - } + model := &m.AlertNotification{ + Id: 1, + Name: "name", + Type: "email", + Settings: bJson, + } - Convey("can parse false value", func() { - bJson.Set("uploadImage", false) + Convey("can parse false value", func() { + bJson.Set("uploadImage", false) - base := NewNotifierBase(model) - So(base.UploadImage, ShouldBeFalse) - }) - - Convey("can parse true value", func() { - bJson.Set("uploadImage", true) - - base := NewNotifierBase(model) - So(base.UploadImage, ShouldBeTrue) - }) - - Convey("default value should be true for backwards compatibility", func() { - base := NewNotifierBase(model) - So(base.UploadImage, ShouldBeTrue) - }) + base := NewNotifierBase(model) + So(base.UploadImage, ShouldBeFalse) }) - Convey("should notify", func() { - Convey("pending -> ok", func() { - context := alerting.NewEvalContext(context.TODO(), &alerting.Rule{ - State: m.AlertStatePending, - }) - context.Rule.State = m.AlertStateOK - timeNow := time.Now() - So(defaultShouldNotify(context, true, 0, &timeNow), ShouldBeFalse) - }) + Convey("can parse true value", func() { + bJson.Set("uploadImage", true) - Convey("ok -> alerting", func() { - context := alerting.NewEvalContext(context.TODO(), &alerting.Rule{ - State: m.AlertStateOK, - }) - context.Rule.State = m.AlertStateAlerting - timeNow := time.Now() - So(defaultShouldNotify(context, true, 0, &timeNow), ShouldBeTrue) - }) + base := NewNotifierBase(model) + So(base.UploadImage, ShouldBeTrue) + }) + + Convey("default value should be true for backwards compatibility", func() { + base := NewNotifierBase(model) + So(base.UploadImage, ShouldBeTrue) }) }) } diff --git a/pkg/services/alerting/result_handler.go b/pkg/services/alerting/result_handler.go index c4c20bd8beb..363d06d1132 100644 --- a/pkg/services/alerting/result_handler.go +++ b/pkg/services/alerting/result_handler.go @@ -95,7 +95,7 @@ func (handler *DefaultResultHandler) Handle(evalContext *EvalContext) error { NotifierId: notifierId, OrgId: evalContext.Rule.OrgId, } - if err := bus.Dispatch(cmd); err != nil { + if err := bus.DispatchCtx(evalContext.Ctx, cmd); err != nil { handler.log.Error("Failed to clean up old notification records", "notifier", notifierId, "alert", evalContext.Rule.Id, "Error", err) } } diff --git a/pkg/services/sqlstore/alert_notification.go b/pkg/services/sqlstore/alert_notification.go index 167b9b3bd61..26362dcb750 100644 --- a/pkg/services/sqlstore/alert_notification.go +++ b/pkg/services/sqlstore/alert_notification.go @@ -2,6 +2,7 @@ package sqlstore import ( "bytes" + "context" "fmt" "strings" "time" @@ -17,9 +18,9 @@ func init() { bus.AddHandler("sql", DeleteAlertNotification) bus.AddHandler("sql", GetAlertNotificationsToSend) bus.AddHandler("sql", GetAllAlertNotifications) - bus.AddHandler("sql", RecordNotificationJournal) - bus.AddHandler("sql", GetLatestNotification) - bus.AddHandler("sql", CleanNotificationJournal) + bus.AddHandlerCtx("sql", RecordNotificationJournal) + bus.AddHandlerCtx("sql", GetLatestNotification) + bus.AddHandlerCtx("sql", CleanNotificationJournal) } func DeleteAlertNotification(cmd *m.DeleteAlertNotificationCommand) error { @@ -228,8 +229,8 @@ func UpdateAlertNotification(cmd *m.UpdateAlertNotificationCommand) error { }) } -func RecordNotificationJournal(cmd *m.RecordNotificationJournalCommand) error { - return inTransaction(func(sess *DBSession) error { +func RecordNotificationJournal(ctx context.Context, cmd *m.RecordNotificationJournalCommand) error { + return inTransactionCtx(ctx, func(sess *DBSession) error { journalEntry := &m.AlertNotificationJournal{ OrgId: cmd.OrgId, AlertId: cmd.AlertId, @@ -246,8 +247,8 @@ func RecordNotificationJournal(cmd *m.RecordNotificationJournalCommand) error { }) } -func GetLatestNotification(cmd *m.GetLatestNotificationQuery) error { - return inTransaction(func(sess *DBSession) error { +func GetLatestNotification(ctx context.Context, cmd *m.GetLatestNotificationQuery) error { + return inTransactionCtx(ctx, func(sess *DBSession) error { notificationJournal := &m.AlertNotificationJournal{} _, err := sess.Desc("alert_notification_journal.sent_at").Limit(1).Where("alert_notification_journal.org_id = ? AND alert_notification_journal.alert_id = ? AND alert_notification_journal.notifier_id = ?", cmd.OrgId, cmd.AlertId, cmd.NotifierId).Get(notificationJournal) if err != nil { @@ -259,8 +260,8 @@ func GetLatestNotification(cmd *m.GetLatestNotificationQuery) error { }) } -func CleanNotificationJournal(cmd *m.CleanNotificationJournalCommand) error { - return inTransaction(func(sess *DBSession) error { +func CleanNotificationJournal(ctx context.Context, cmd *m.CleanNotificationJournalCommand) error { + return inTransactionCtx(ctx, func(sess *DBSession) error { sql := "DELETE FROM alert_notification_journal WHERE notification_journal.org_id = ? AND alert_notification_journal.alert_id = ? AND alert_notification_journal.notifier_id = ?" _, err := sess.Exec(sql, cmd.OrgId, cmd.AlertId, cmd.NotifierId) return err diff --git a/pkg/services/sqlstore/transactions.go b/pkg/services/sqlstore/transactions.go index f72b0bb8500..59290f83121 100644 --- a/pkg/services/sqlstore/transactions.go +++ b/pkg/services/sqlstore/transactions.go @@ -103,3 +103,7 @@ func inTransactionWithRetryCtx(ctx context.Context, callback dbTransactionFunc, func inTransaction(callback dbTransactionFunc) error { return inTransactionWithRetry(callback, 0) } + +func inTransactionCtx(ctx context.Context, callback dbTransactionFunc) error { + return inTransactionWithRetryCtx(ctx, callback, 0) +} From 12bf5c225a98121a9198ffd54a80b609e8364657 Mon Sep 17 00:00:00 2001 From: bergquist Date: Fri, 15 Jun 2018 16:27:20 +0200 Subject: [PATCH 070/786] tests for defaultShouldNotify --- pkg/services/alerting/notifiers/base.go | 4 ++ pkg/services/alerting/notifiers/base_test.go | 45 +++++++++++++++++--- pkg/services/sqlstore/alert_notification.go | 2 +- 3 files changed, 43 insertions(+), 8 deletions(-) diff --git a/pkg/services/alerting/notifiers/base.go b/pkg/services/alerting/notifiers/base.go index 8450816f97e..8178054f4d3 100644 --- a/pkg/services/alerting/notifiers/base.go +++ b/pkg/services/alerting/notifiers/base.go @@ -78,6 +78,10 @@ func (n *NotifierBase) ShouldNotify(c *alerting.EvalContext) bool { return false } + // this currently serves two purposes. + // 1. make sure failed notifications try again + // 2. make sure we send notifications if no previous exist + // this should be refactored //Carl Bergquist if !cmd.Result.Success { return true } diff --git a/pkg/services/alerting/notifiers/base_test.go b/pkg/services/alerting/notifiers/base_test.go index b7395030e5b..96c80cf03bc 100644 --- a/pkg/services/alerting/notifiers/base_test.go +++ b/pkg/services/alerting/notifiers/base_test.go @@ -13,30 +13,61 @@ import ( func TestShouldSendAlertNotification(t *testing.T) { tcs := []struct { - prevState m.AlertStateType - newState m.AlertStateType - expected bool + name string + prevState m.AlertStateType + newState m.AlertStateType + expected bool + sendReminder bool }{ { + name: "pending -> ok should not trigger an notification", newState: m.AlertStatePending, prevState: m.AlertStateOK, expected: false, }, { + name: "ok -> alerting should trigger an notification", newState: m.AlertStateOK, prevState: m.AlertStateAlerting, expected: true, }, + { + name: "ok -> pending should not trigger an notification", + newState: m.AlertStateOK, + prevState: m.AlertStatePending, + expected: false, + }, + { + name: "ok -> ok should not trigger an notification", + newState: m.AlertStateOK, + prevState: m.AlertStateOK, + expected: false, + sendReminder: false, + }, + { + name: "ok -> alerting should not trigger an notification", + newState: m.AlertStateOK, + prevState: m.AlertStateAlerting, + expected: true, + sendReminder: true, + }, + { + name: "ok -> ok with reminder should not trigger an notification", + newState: m.AlertStateOK, + prevState: m.AlertStateOK, + expected: false, + sendReminder: true, + }, } for _, tc := range tcs { - context := alerting.NewEvalContext(context.TODO(), &alerting.Rule{ + evalContext := alerting.NewEvalContext(context.TODO(), &alerting.Rule{ State: tc.newState, }) - context.Rule.State = tc.prevState + evalContext.Rule.State = tc.prevState timeNow := time.Now() - if defaultShouldNotify(context, true, 0, &timeNow) != tc.expected { - t.Errorf("expected %v to return %v", tc, tc.expected) + if defaultShouldNotify(evalContext, true, 0, &timeNow) != tc.expected { + t.Errorf("failed %s. expected %+v to return %v", tc.name, tc, tc.expected) } } } diff --git a/pkg/services/sqlstore/alert_notification.go b/pkg/services/sqlstore/alert_notification.go index 26362dcb750..5f08a70eff3 100644 --- a/pkg/services/sqlstore/alert_notification.go +++ b/pkg/services/sqlstore/alert_notification.go @@ -262,7 +262,7 @@ func GetLatestNotification(ctx context.Context, cmd *m.GetLatestNotificationQuer func CleanNotificationJournal(ctx context.Context, cmd *m.CleanNotificationJournalCommand) error { return inTransactionCtx(ctx, func(sess *DBSession) error { - sql := "DELETE FROM alert_notification_journal WHERE notification_journal.org_id = ? AND alert_notification_journal.alert_id = ? AND alert_notification_journal.notifier_id = ?" + sql := "DELETE FROM alert_notification_journal WHERE alert_notification_journal.org_id = ? AND alert_notification_journal.alert_id = ? AND alert_notification_journal.notifier_id = ?" _, err := sess.Exec(sql, cmd.OrgId, cmd.AlertId, cmd.NotifierId) return err }) From 72224dbe377e5c065624065724ab9ebfb6011ea3 Mon Sep 17 00:00:00 2001 From: bergquist Date: Fri, 15 Jun 2018 16:53:35 +0200 Subject: [PATCH 071/786] adds info about eval/reminder interval --- .../features/alerting/partials/notification_edit.html | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/public/app/features/alerting/partials/notification_edit.html b/public/app/features/alerting/partials/notification_edit.html index 2a2fbb131b8..7132ed41f3c 100644 --- a/public/app/features/alerting/partials/notification_edit.html +++ b/public/app/features/alerting/partials/notification_edit.html @@ -39,6 +39,11 @@ checked="ctrl.model.sendReminder" tooltip="Choose to either notify on state change or at every interval"> +
+ + Alert reminders are sent after rules are evaluated. Therefore the alert rule interval has to be lower than the reminder frequency + +
Send reminder every @@ -50,8 +55,8 @@ ng-if="ctrl.model.sendReminder" spellcheck='false' placeholder='15m'> - - Specify at what interval you want reminder's about this alerting beeing triggered. + + Specify at what interval you want reminder's about this alerting being triggered. Ex. 60s, 10m, 30m, 1h
From c21938d4c44367a35d7004f5919023f50b467f73 Mon Sep 17 00:00:00 2001 From: bergquist Date: Sat, 16 Jun 2018 00:03:13 +0200 Subject: [PATCH 072/786] use epoch to compare timestamp --- pkg/models/alert_notifications.go | 4 +-- pkg/services/alerting/notifier.go | 2 +- pkg/services/alerting/notifiers/base.go | 6 ++-- pkg/services/alerting/notifiers/base_test.go | 3 +- pkg/services/sqlstore/migrations/alert_mig.go | 36 +++++++++---------- 5 files changed, 25 insertions(+), 26 deletions(-) diff --git a/pkg/models/alert_notifications.go b/pkg/models/alert_notifications.go index 8df7a830d8b..6be2b02c96f 100644 --- a/pkg/models/alert_notifications.go +++ b/pkg/models/alert_notifications.go @@ -80,7 +80,7 @@ type AlertNotificationJournal struct { OrgId int64 AlertId int64 NotifierId int64 - SentAt time.Time + SentAt int64 Success bool } @@ -88,7 +88,7 @@ type RecordNotificationJournalCommand struct { OrgId int64 AlertId int64 NotifierId int64 - SentAt time.Time + SentAt int64 Success bool } diff --git a/pkg/services/alerting/notifier.go b/pkg/services/alerting/notifier.go index a3d016211c3..61526ed642c 100644 --- a/pkg/services/alerting/notifier.go +++ b/pkg/services/alerting/notifier.go @@ -87,7 +87,7 @@ func (n *notificationService) sendNotifications(evalContext *EvalContext, notifi OrgId: evalContext.Rule.OrgId, AlertId: evalContext.Rule.Id, NotifierId: not.GetNotifierId(), - SentAt: time.Now(), + SentAt: time.Now().Unix(), Success: success, } diff --git a/pkg/services/alerting/notifiers/base.go b/pkg/services/alerting/notifiers/base.go index 8178054f4d3..6245650ab4e 100644 --- a/pkg/services/alerting/notifiers/base.go +++ b/pkg/services/alerting/notifiers/base.go @@ -41,14 +41,14 @@ func NewNotifierBase(model *models.AlertNotification) NotifierBase { } } -func defaultShouldNotify(context *alerting.EvalContext, sendReminder bool, frequency time.Duration, lastNotify *time.Time) bool { +func defaultShouldNotify(context *alerting.EvalContext, sendReminder bool, frequency time.Duration, lastNotify time.Time) bool { // Only notify on state change. if context.PrevAlertState == context.Rule.State && !sendReminder { return false } // Do not notify if interval has not elapsed - if sendReminder && lastNotify != nil && lastNotify.Add(frequency).After(time.Now()) { + if sendReminder && !lastNotify.IsZero() && lastNotify.Add(frequency).After(time.Now()) { return false } @@ -86,7 +86,7 @@ func (n *NotifierBase) ShouldNotify(c *alerting.EvalContext) bool { return true } - return defaultShouldNotify(c, n.SendReminder, n.Frequency, &cmd.Result.SentAt) + return defaultShouldNotify(c, n.SendReminder, n.Frequency, time.Unix(cmd.Result.SentAt, 0)) } func (n *NotifierBase) GetType() string { diff --git a/pkg/services/alerting/notifiers/base_test.go b/pkg/services/alerting/notifiers/base_test.go index 96c80cf03bc..5b75ea4d59b 100644 --- a/pkg/services/alerting/notifiers/base_test.go +++ b/pkg/services/alerting/notifiers/base_test.go @@ -65,8 +65,7 @@ func TestShouldSendAlertNotification(t *testing.T) { State: tc.newState, }) evalContext.Rule.State = tc.prevState - timeNow := time.Now() - if defaultShouldNotify(evalContext, true, 0, &timeNow) != tc.expected { + if defaultShouldNotify(evalContext, true, 0, time.Now()) != tc.expected { t.Errorf("failed %s. expected %+v to return %v", tc.name, tc, tc.expected) } } diff --git a/pkg/services/sqlstore/migrations/alert_mig.go b/pkg/services/sqlstore/migrations/alert_mig.go index e58959929d1..e27e64c6124 100644 --- a/pkg/services/sqlstore/migrations/alert_mig.go +++ b/pkg/services/sqlstore/migrations/alert_mig.go @@ -74,24 +74,6 @@ func addAlertMigrations(mg *Migrator) { mg.AddMigration("add index alert_notification org_id & name", NewAddIndexMigration(alert_notification, alert_notification.Indices[0])) - notification_journal := Table{ - Name: "alert_notification_journal", - Columns: []*Column{ - {Name: "id", Type: DB_BigInt, IsPrimaryKey: true, IsAutoIncrement: true}, - {Name: "org_id", Type: DB_BigInt, Nullable: false}, - {Name: "alert_id", Type: DB_BigInt, Nullable: false}, - {Name: "notifier_id", Type: DB_BigInt, Nullable: false}, - {Name: "sent_at", Type: DB_DateTime, Nullable: false}, - {Name: "success", Type: DB_Bool, Nullable: false}, - }, - Indices: []*Index{ - {Cols: []string{"org_id", "alert_id", "notifier_id"}, Type: IndexType}, - }, - } - - mg.AddMigration("create notification_journal table v1", NewAddTableMigration(notification_journal)) - mg.AddMigration("add index notification_journal org_id & alert_id & notifier_id", NewAddIndexMigration(notification_journal, notification_journal.Indices[0])) - mg.AddMigration("Update alert table charset", NewTableCharsetMigration("alert", []*Column{ {Name: "name", Type: DB_NVarchar, Length: 255, Nullable: false}, {Name: "message", Type: DB_Text, Nullable: false}, @@ -107,4 +89,22 @@ func addAlertMigrations(mg *Migrator) { {Name: "type", Type: DB_NVarchar, Length: 255, Nullable: false}, {Name: "settings", Type: DB_Text, Nullable: false}, })) + + notification_journal := Table{ + Name: "alert_notification_journal", + Columns: []*Column{ + {Name: "id", Type: DB_BigInt, IsPrimaryKey: true, IsAutoIncrement: true}, + {Name: "org_id", Type: DB_BigInt, Nullable: false}, + {Name: "alert_id", Type: DB_BigInt, Nullable: false}, + {Name: "notifier_id", Type: DB_BigInt, Nullable: false}, + {Name: "sent_at", Type: DB_BigInt, Nullable: false}, + {Name: "success", Type: DB_Bool, Nullable: false}, + }, + Indices: []*Index{ + {Cols: []string{"org_id", "alert_id", "notifier_id"}, Type: IndexType}, + }, + } + + mg.AddMigration("create notification_journal table v1", NewAddTableMigration(notification_journal)) + mg.AddMigration("add index notification_journal org_id & alert_id & notifier_id", NewAddIndexMigration(notification_journal, notification_journal.Indices[0])) } From 83a12afc07ef0d6891e9d9f76736359ec067c024 Mon Sep 17 00:00:00 2001 From: bergquist Date: Sat, 16 Jun 2018 11:27:04 +0200 Subject: [PATCH 073/786] adds tests for journaling sql operations --- pkg/models/alert_notifications.go | 1 + pkg/services/alerting/notifiers/base.go | 11 ++--- pkg/services/sqlstore/alert_notification.go | 13 ++++-- .../sqlstore/alert_notification_test.go | 43 +++++++++++++++++++ 4 files changed, 60 insertions(+), 8 deletions(-) diff --git a/pkg/models/alert_notifications.go b/pkg/models/alert_notifications.go index 6be2b02c96f..42d33d5ed22 100644 --- a/pkg/models/alert_notifications.go +++ b/pkg/models/alert_notifications.go @@ -9,6 +9,7 @@ import ( var ( ErrNotificationFrequencyNotFound = errors.New("Notification frequency not specified") + ErrJournalingNotFound = errors.New("alert notification journaling not found") ) type AlertNotification struct { diff --git a/pkg/services/alerting/notifiers/base.go b/pkg/services/alerting/notifiers/base.go index 6245650ab4e..4869c40f436 100644 --- a/pkg/services/alerting/notifiers/base.go +++ b/pkg/services/alerting/notifiers/base.go @@ -73,15 +73,16 @@ func (n *NotifierBase) ShouldNotify(c *alerting.EvalContext) bool { NotifierId: n.Id, } - if err := bus.DispatchCtx(c.Ctx, cmd); err != nil { + err := bus.DispatchCtx(c.Ctx, cmd) + if err != nil { n.log.Error("Could not determine last time alert notifier fired", "Alert name", c.Rule.Name, "Error", err) return false } - // this currently serves two purposes. - // 1. make sure failed notifications try again - // 2. make sure we send notifications if no previous exist - // this should be refactored //Carl Bergquist + if err == models.ErrJournalingNotFound { + return true + } + if !cmd.Result.Success { return true } diff --git a/pkg/services/sqlstore/alert_notification.go b/pkg/services/sqlstore/alert_notification.go index 5f08a70eff3..3f2ca109c1a 100644 --- a/pkg/services/sqlstore/alert_notification.go +++ b/pkg/services/sqlstore/alert_notification.go @@ -249,13 +249,20 @@ func RecordNotificationJournal(ctx context.Context, cmd *m.RecordNotificationJou func GetLatestNotification(ctx context.Context, cmd *m.GetLatestNotificationQuery) error { return inTransactionCtx(ctx, func(sess *DBSession) error { - notificationJournal := &m.AlertNotificationJournal{} - _, err := sess.Desc("alert_notification_journal.sent_at").Limit(1).Where("alert_notification_journal.org_id = ? AND alert_notification_journal.alert_id = ? AND alert_notification_journal.notifier_id = ?", cmd.OrgId, cmd.AlertId, cmd.NotifierId).Get(notificationJournal) + nj := &m.AlertNotificationJournal{} + _, err := sess.Desc("alert_notification_journal.sent_at"). + Limit(1). + Where("alert_notification_journal.org_id = ? AND alert_notification_journal.alert_id = ? AND alert_notification_journal.notifier_id = ?", cmd.OrgId, cmd.AlertId, cmd.NotifierId).Get(nj) + if err != nil { return err } - cmd.Result = notificationJournal + if nj.AlertId == 0 && nj.Id == 0 && nj.NotifierId == 0 && nj.OrgId == 0 { + return m.ErrJournalingNotFound + } + + cmd.Result = nj return nil }) } diff --git a/pkg/services/sqlstore/alert_notification_test.go b/pkg/services/sqlstore/alert_notification_test.go index fe7f02b22b0..aba437f427e 100644 --- a/pkg/services/sqlstore/alert_notification_test.go +++ b/pkg/services/sqlstore/alert_notification_test.go @@ -1,6 +1,7 @@ package sqlstore import ( + "context" "testing" "github.com/grafana/grafana/pkg/components/simplejson" @@ -12,6 +13,48 @@ func TestAlertNotificationSQLAccess(t *testing.T) { Convey("Testing Alert notification sql access", t, func() { InitTestDB(t) + Convey("Alert notification journal", func() { + var alertId int64 = 5 + var orgId int64 = 5 + var notifierId int64 = 5 + + Convey("Getting last journal should raise error if no one exists", func() { + query := &m.GetLatestNotificationQuery{AlertId: alertId, OrgId: orgId, NotifierId: notifierId} + err := GetLatestNotification(context.Background(), query) + So(err, ShouldEqual, m.ErrJournalingNotFound) + + Convey("shoulbe be able to record two journaling events", func() { + createCmd := &m.RecordNotificationJournalCommand{AlertId: alertId, NotifierId: notifierId, OrgId: orgId, Success: true, SentAt: 1} + + err := RecordNotificationJournal(context.Background(), createCmd) + So(err, ShouldBeNil) + + createCmd.SentAt += 1000 //increase epoch + + err = RecordNotificationJournal(context.Background(), createCmd) + So(err, ShouldBeNil) + + Convey("get last journaling event", func() { + err := GetLatestNotification(context.Background(), query) + So(err, ShouldBeNil) + So(query.Result.SentAt, ShouldEqual, 1001) + + Convey("be able to clear all journaling for an notifier", func() { + cmd := &m.CleanNotificationJournalCommand{AlertId: alertId, NotifierId: notifierId, OrgId: orgId} + err := CleanNotificationJournal(context.Background(), cmd) + So(err, ShouldBeNil) + + Convey("querying for last junaling should raise error", func() { + query := &m.GetLatestNotificationQuery{AlertId: alertId, OrgId: orgId, NotifierId: notifierId} + err := GetLatestNotification(context.Background(), query) + So(err, ShouldEqual, m.ErrJournalingNotFound) + }) + }) + }) + }) + }) + }) + Convey("Alert notifications should be empty", func() { cmd := &m.GetAlertNotificationsQuery{ OrgId: 2, From 757e2b0b7ee264079853a92fe70a706a16230984 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Niemann?= Date: Mon, 18 Jun 2018 10:59:44 +0200 Subject: [PATCH 074/786] added comment to reason the id tag --- public/app/features/dashboard/dashgrid/DashboardGrid.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/public/app/features/dashboard/dashgrid/DashboardGrid.tsx b/public/app/features/dashboard/dashgrid/DashboardGrid.tsx index 457ad4ef56c..9a451798ff7 100644 --- a/public/app/features/dashboard/dashgrid/DashboardGrid.tsx +++ b/public/app/features/dashboard/dashgrid/DashboardGrid.tsx @@ -177,6 +177,7 @@ export class DashboardGrid extends React.Component { for (let panel of this.dashboard.panels) { const panelClasses = classNames({ panel: true, 'panel--fullscreen': panel.fullscreen }); panelElements.push( + /** panel-id is set for html bookmarks */
From 8ff538be074f228239e1694c617fc57c89d5d5cf Mon Sep 17 00:00:00 2001 From: bergquist Date: Tue, 26 Jun 2018 14:13:45 +0200 Subject: [PATCH 075/786] notifier: handle known error first --- pkg/services/alerting/notifiers/base.go | 8 ++--- pkg/services/alerting/notifiers/base_test.go | 38 ++++++++++++++++++++ 2 files changed, 42 insertions(+), 4 deletions(-) diff --git a/pkg/services/alerting/notifiers/base.go b/pkg/services/alerting/notifiers/base.go index 4869c40f436..31ec77cbf25 100644 --- a/pkg/services/alerting/notifiers/base.go +++ b/pkg/services/alerting/notifiers/base.go @@ -74,15 +74,15 @@ func (n *NotifierBase) ShouldNotify(c *alerting.EvalContext) bool { } err := bus.DispatchCtx(c.Ctx, cmd) + if err == models.ErrJournalingNotFound { + return true + } + if err != nil { n.log.Error("Could not determine last time alert notifier fired", "Alert name", c.Rule.Name, "Error", err) return false } - if err == models.ErrJournalingNotFound { - return true - } - if !cmd.Result.Success { return true } diff --git a/pkg/services/alerting/notifiers/base_test.go b/pkg/services/alerting/notifiers/base_test.go index 5b75ea4d59b..3fd23b69c6e 100644 --- a/pkg/services/alerting/notifiers/base_test.go +++ b/pkg/services/alerting/notifiers/base_test.go @@ -2,9 +2,12 @@ package notifiers import ( "context" + "errors" "testing" "time" + "github.com/grafana/grafana/pkg/bus" + "github.com/grafana/grafana/pkg/components/simplejson" m "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/alerting" @@ -64,6 +67,7 @@ func TestShouldSendAlertNotification(t *testing.T) { evalContext := alerting.NewEvalContext(context.TODO(), &alerting.Rule{ State: tc.newState, }) + evalContext.Rule.State = tc.prevState if defaultShouldNotify(evalContext, true, 0, time.Now()) != tc.expected { t.Errorf("failed %s. expected %+v to return %v", tc.name, tc, tc.expected) @@ -71,6 +75,40 @@ func TestShouldSendAlertNotification(t *testing.T) { } } +func TestShouldNotifyWhenNoJournalingIsFound(t *testing.T) { + Convey("base notifier", t, func() { + bus.ClearBusHandlers() + + notifier := NewNotifierBase(&m.AlertNotification{ + Id: 1, + Name: "name", + Type: "email", + Settings: simplejson.New(), + }) + evalContext := alerting.NewEvalContext(context.TODO(), &alerting.Rule{}) + + Convey("should notify if no journaling is found", func() { + bus.AddHandlerCtx("", func(ctx context.Context, q *m.GetLatestNotificationQuery) error { + return m.ErrJournalingNotFound + }) + + if !notifier.ShouldNotify(evalContext) { + t.Errorf("should send notifications when ErrJournalingNotFound is returned") + } + }) + + Convey("should not notify query returns error", func() { + bus.AddHandlerCtx("", func(ctx context.Context, q *m.GetLatestNotificationQuery) error { + return errors.New("some kind of error unknown error") + }) + + if notifier.ShouldNotify(evalContext) { + t.Errorf("should not send notifications when query returns error") + } + }) + }) +} + func TestBaseNotifier(t *testing.T) { Convey("default constructor for notifiers", t, func() { bJson := simplejson.New() From 396f8e6464a38e6ac925ecb8465a71e5118b79c5 Mon Sep 17 00:00:00 2001 From: bergquist Date: Fri, 29 Jun 2018 15:15:31 +0200 Subject: [PATCH 076/786] notifications: read without tran, write with tran --- pkg/services/alerting/interfaces.go | 7 +++++-- pkg/services/alerting/notifier.go | 6 +++--- pkg/services/alerting/notifiers/alertmanager.go | 3 ++- pkg/services/alerting/notifiers/base.go | 5 +++-- pkg/services/alerting/notifiers/base_test.go | 4 ++-- pkg/services/sqlstore/alert_notification.go | 1 + 6 files changed, 16 insertions(+), 10 deletions(-) diff --git a/pkg/services/alerting/interfaces.go b/pkg/services/alerting/interfaces.go index b4376191df0..46f8b3c769c 100644 --- a/pkg/services/alerting/interfaces.go +++ b/pkg/services/alerting/interfaces.go @@ -1,6 +1,9 @@ package alerting -import "time" +import ( + "context" + "time" +) type EvalHandler interface { Eval(evalContext *EvalContext) @@ -17,7 +20,7 @@ type Notifier interface { NeedsImage() bool // ShouldNotify checks this evaluation should send an alert notification - ShouldNotify(evalContext *EvalContext) bool + ShouldNotify(ctx context.Context, evalContext *EvalContext) bool GetNotifierId() int64 GetIsDefault() bool diff --git a/pkg/services/alerting/notifier.go b/pkg/services/alerting/notifier.go index 61526ed642c..a19e44a8f99 100644 --- a/pkg/services/alerting/notifier.go +++ b/pkg/services/alerting/notifier.go @@ -72,7 +72,7 @@ func (n *notificationService) sendNotifications(evalContext *EvalContext, notifi // Verify that we can send the notification again // but this time within the same transaction. - if !evalContext.IsTestRun && !not.ShouldNotify(evalContext) { + if !evalContext.IsTestRun && !not.ShouldNotify(context.Background(), evalContext) { return nil } @@ -91,7 +91,7 @@ func (n *notificationService) sendNotifications(evalContext *EvalContext, notifi Success: success, } - return bus.DispatchCtx(evalContext.Ctx, cmd) + return bus.DispatchCtx(ctx, cmd) }) }) } @@ -149,7 +149,7 @@ func (n *notificationService) getNeededNotifiers(orgId int64, notificationIds [] return nil, err } - if not.ShouldNotify(evalContext) { + if not.ShouldNotify(evalContext.Ctx, evalContext) { result = append(result, not) } } diff --git a/pkg/services/alerting/notifiers/alertmanager.go b/pkg/services/alerting/notifiers/alertmanager.go index 42ffa9b2d6e..9826dd1dffb 100644 --- a/pkg/services/alerting/notifiers/alertmanager.go +++ b/pkg/services/alerting/notifiers/alertmanager.go @@ -1,6 +1,7 @@ package notifiers import ( + "context" "time" "github.com/grafana/grafana/pkg/bus" @@ -45,7 +46,7 @@ type AlertmanagerNotifier struct { log log.Logger } -func (this *AlertmanagerNotifier) ShouldNotify(evalContext *alerting.EvalContext) bool { +func (this *AlertmanagerNotifier) ShouldNotify(ctx context.Context, evalContext *alerting.EvalContext) bool { this.log.Debug("Should notify", "ruleId", evalContext.Rule.Id, "state", evalContext.Rule.State, "previousState", evalContext.PrevAlertState) // Do not notify when we become OK for the first time. diff --git a/pkg/services/alerting/notifiers/base.go b/pkg/services/alerting/notifiers/base.go index 31ec77cbf25..ca011356247 100644 --- a/pkg/services/alerting/notifiers/base.go +++ b/pkg/services/alerting/notifiers/base.go @@ -1,6 +1,7 @@ package notifiers import ( + "context" "time" "github.com/grafana/grafana/pkg/bus" @@ -66,14 +67,14 @@ func defaultShouldNotify(context *alerting.EvalContext, sendReminder bool, frequ } // ShouldNotify checks this evaluation should send an alert notification -func (n *NotifierBase) ShouldNotify(c *alerting.EvalContext) bool { +func (n *NotifierBase) ShouldNotify(ctx context.Context, c *alerting.EvalContext) bool { cmd := &models.GetLatestNotificationQuery{ OrgId: c.Rule.OrgId, AlertId: c.Rule.Id, NotifierId: n.Id, } - err := bus.DispatchCtx(c.Ctx, cmd) + err := bus.DispatchCtx(ctx, cmd) if err == models.ErrJournalingNotFound { return true } diff --git a/pkg/services/alerting/notifiers/base_test.go b/pkg/services/alerting/notifiers/base_test.go index 3fd23b69c6e..57b82f32466 100644 --- a/pkg/services/alerting/notifiers/base_test.go +++ b/pkg/services/alerting/notifiers/base_test.go @@ -92,7 +92,7 @@ func TestShouldNotifyWhenNoJournalingIsFound(t *testing.T) { return m.ErrJournalingNotFound }) - if !notifier.ShouldNotify(evalContext) { + if !notifier.ShouldNotify(context.Background(), evalContext) { t.Errorf("should send notifications when ErrJournalingNotFound is returned") } }) @@ -102,7 +102,7 @@ func TestShouldNotifyWhenNoJournalingIsFound(t *testing.T) { return errors.New("some kind of error unknown error") }) - if notifier.ShouldNotify(evalContext) { + if notifier.ShouldNotify(context.Background(), evalContext) { t.Errorf("should not send notifications when query returns error") } }) diff --git a/pkg/services/sqlstore/alert_notification.go b/pkg/services/sqlstore/alert_notification.go index 3f2ca109c1a..8fb1e2212a9 100644 --- a/pkg/services/sqlstore/alert_notification.go +++ b/pkg/services/sqlstore/alert_notification.go @@ -250,6 +250,7 @@ func RecordNotificationJournal(ctx context.Context, cmd *m.RecordNotificationJou func GetLatestNotification(ctx context.Context, cmd *m.GetLatestNotificationQuery) error { return inTransactionCtx(ctx, func(sess *DBSession) error { nj := &m.AlertNotificationJournal{} + _, err := sess.Desc("alert_notification_journal.sent_at"). Limit(1). Where("alert_notification_journal.org_id = ? AND alert_notification_journal.alert_id = ? AND alert_notification_journal.notifier_id = ?", cmd.OrgId, cmd.AlertId, cmd.NotifierId).Get(nj) From e91e3ea771228af267178f6fd927b4afcf3b6e75 Mon Sep 17 00:00:00 2001 From: bergquist Date: Fri, 29 Jun 2018 16:16:09 +0200 Subject: [PATCH 077/786] notifications: send notifications synchronous --- pkg/services/alerting/notifier.go | 54 +++++++++++++++---------------- 1 file changed, 26 insertions(+), 28 deletions(-) diff --git a/pkg/services/alerting/notifier.go b/pkg/services/alerting/notifier.go index 7fe97596494..fb2933f6e26 100644 --- a/pkg/services/alerting/notifier.go +++ b/pkg/services/alerting/notifier.go @@ -6,8 +6,6 @@ import ( "fmt" "time" - "golang.org/x/sync/errgroup" - "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/components/imguploader" "github.com/grafana/grafana/pkg/log" @@ -61,42 +59,42 @@ func (n *notificationService) SendIfNeeded(context *EvalContext) error { } func (n *notificationService) sendNotifications(evalContext *EvalContext, notifiers []Notifier) error { - g, _ := errgroup.WithContext(evalContext.Ctx) - for _, notifier := range notifiers { - not := notifier //avoid updating scope variable in go routine + not := notifier - g.Go(func() error { - return bus.InTransaction(evalContext.Ctx, func(ctx context.Context) error { - n.log.Debug("trying to send notification", "id", not.GetNotifierId()) + err := bus.InTransaction(evalContext.Ctx, func(ctx context.Context) error { + n.log.Debug("trying to send notification", "id", not.GetNotifierId()) - // Verify that we can send the notification again - // but this time within the same transaction. - if !evalContext.IsTestRun && !not.ShouldNotify(context.Background(), evalContext) { - return nil - } + // Verify that we can send the notification again + // but this time within the same transaction. + if !evalContext.IsTestRun && !not.ShouldNotify(context.Background(), evalContext) { + return nil + } - n.log.Debug("Sending notification", "type", not.GetType(), "id", not.GetNotifierId(), "isDefault", not.GetIsDefault()) - metrics.M_Alerting_Notification_Sent.WithLabelValues(not.GetType()).Inc() + n.log.Debug("Sending notification", "type", not.GetType(), "id", not.GetNotifierId(), "isDefault", not.GetIsDefault()) + metrics.M_Alerting_Notification_Sent.WithLabelValues(not.GetType()).Inc() - //send notification - success := not.Notify(evalContext) == nil + //send notification + success := not.Notify(evalContext) == nil - //write result to db. - cmd := &m.RecordNotificationJournalCommand{ - OrgId: evalContext.Rule.OrgId, - AlertId: evalContext.Rule.Id, - NotifierId: not.GetNotifierId(), - SentAt: time.Now().Unix(), - Success: success, - } + //write result to db. + cmd := &m.RecordNotificationJournalCommand{ + OrgId: evalContext.Rule.OrgId, + AlertId: evalContext.Rule.Id, + NotifierId: not.GetNotifierId(), + SentAt: time.Now().Unix(), + Success: success, + } - return bus.DispatchCtx(ctx, cmd) - }) + return bus.DispatchCtx(ctx, cmd) }) + + if err != nil { + return err + } } - return g.Wait() + return nil } func (n *notificationService) uploadImage(context *EvalContext) (err error) { From 410449b5e75699e439a19c3112732e42821b1198 Mon Sep 17 00:00:00 2001 From: Sven Klemm Date: Sat, 30 Jun 2018 11:11:34 +0200 Subject: [PATCH 078/786] use sqlPart for ui parts --- .../postgres/partials/query.editor.html | 9 +- .../plugins/datasource/postgres/query_ctrl.ts | 202 +++++++----------- .../plugins/datasource/postgres/query_part.ts | 29 ++- 3 files changed, 106 insertions(+), 134 deletions(-) diff --git a/public/app/plugins/datasource/postgres/partials/query.editor.html b/public/app/plugins/datasource/postgres/partials/query.editor.html index 220f11fdfef..ea5b4b4e184 100644 --- a/public/app/plugins/datasource/postgres/partials/query.editor.html +++ b/public/app/plugins/datasource/postgres/partials/query.editor.html @@ -18,8 +18,13 @@
-
- +
+ + +
+ +
+
diff --git a/public/app/plugins/datasource/postgres/query_ctrl.ts b/public/app/plugins/datasource/postgres/query_ctrl.ts index dd1da1c75cf..1eb8b3ef80a 100644 --- a/public/app/plugins/datasource/postgres/query_ctrl.ts +++ b/public/app/plugins/datasource/postgres/query_ctrl.ts @@ -1,8 +1,8 @@ -import angular from 'angular'; import _ from 'lodash'; import { PostgresQueryBuilder } from './query_builder'; import { QueryCtrl } from 'app/plugins/sdk'; import PostgresQuery from './postgres_query'; +import sqlPart from './query_part'; export interface QueryMeta { sql: string; @@ -30,11 +30,11 @@ export class PostgresQueryCtrl extends QueryCtrl { schemaSegment: any; tableSegment: any; whereSegments: any; + whereAdd: any; timeColumnSegment: any; metricColumnSegment: any; selectMenu: any; groupBySegment: any; - removeWhereFilterSegment: any; /** @ngInject **/ constructor($scope, $injector, private templateSrv, private $q, private uiSegmentSrv) { @@ -55,12 +55,12 @@ export class PostgresQueryCtrl extends QueryCtrl { } } - this.schemaSegment= uiSegmentSrv.newSegment(this.target.schema); + this.schemaSegment = uiSegmentSrv.newSegment(this.target.schema); if (!this.target.table) { - this.tableSegment = uiSegmentSrv.newSegment({value: 'select table',fake: true}); + this.tableSegment = uiSegmentSrv.newSegment({ value: 'select table', fake: true }); } else { - this.tableSegment= uiSegmentSrv.newSegment(this.target.table); + this.tableSegment = uiSegmentSrv.newSegment(this.target.table); } this.timeColumnSegment = uiSegmentSrv.newSegment(this.target.timeColumn); @@ -68,23 +68,19 @@ export class PostgresQueryCtrl extends QueryCtrl { this.buildSelectMenu(); this.buildWhereSegments(); + this.whereAdd = this.uiSegmentSrv.newPlusButton(); this.groupBySegment = this.uiSegmentSrv.newPlusButton(); - this.removeWhereFilterSegment = uiSegmentSrv.newSegment({ - fake: true, - value: '-- remove filter --', - }); this.panelCtrl.events.on('data-received', this.onDataReceived.bind(this), $scope); this.panelCtrl.events.on('data-error', this.onDataError.bind(this), $scope); - } buildSelectMenu() { this.selectMenu = [ - {text: "aggregate", value: "aggregate"}, - {text: "math", value: "math"}, - {text: "alias", value: "alias"}, - {text: "column", value: "column"}, + { text: 'Aggregate', value: 'aggregate' }, + { text: 'Math', value: 'math' }, + { text: 'Alias', value: 'alias' }, + { text: 'Column', value: 'column' }, ]; } @@ -108,14 +104,14 @@ export class PostgresQueryCtrl extends QueryCtrl { getTimeColumnSegments() { return this.datasource - .metricFindQuery(this.queryBuilder.buildColumnQuery("time")) + .metricFindQuery(this.queryBuilder.buildColumnQuery('time')) .then(this.transformToSegments(true)) .catch(this.handleQueryError.bind(this)); } getMetricColumnSegments() { return this.datasource - .metricFindQuery(this.queryBuilder.buildColumnQuery("metric")) + .metricFindQuery(this.queryBuilder.buildColumnQuery('metric')) .then(this.transformToSegments(true)) .catch(this.handleQueryError.bind(this)); } @@ -186,7 +182,7 @@ export class PostgresQueryCtrl extends QueryCtrl { } addSelectPart(selectParts, cat, subitem) { - if ("submenu" in cat) { + if ('submenu' in cat) { this.queryModel.addSelectPart(selectParts, subitem.value); } else { this.queryModel.addSelectPart(selectParts, cat.value); @@ -198,18 +194,17 @@ export class PostgresQueryCtrl extends QueryCtrl { switch (evt.name) { case 'get-param-options': { switch (part.def.type) { - case "aggregate": + case 'aggregate': return this.datasource .metricFindQuery(this.queryBuilder.buildAggregateQuery()) .then(this.transformToSegments(false)) .catch(this.handleQueryError.bind(this)); - case "column": + case 'column': return this.datasource - .metricFindQuery(this.queryBuilder.buildColumnQuery("value")) + .metricFindQuery(this.queryBuilder.buildColumnQuery('value')) .then(this.transformToSegments(true)) .catch(this.handleQueryError.bind(this)); } - } case 'part-param-changed': { this.panelCtrl.refresh(); @@ -251,125 +246,75 @@ export class PostgresQueryCtrl extends QueryCtrl { buildWhereSegments() { this.whereSegments = []; - for (let constraint of this.target.where) { - - if (constraint.condition) { - this.whereSegments.push(this.uiSegmentSrv.newCondition(constraint.condition)); - } - this.whereSegments.push(this.uiSegmentSrv.newKey(constraint.key)); - this.whereSegments.push(this.uiSegmentSrv.newOperator(constraint.operator)); - this.whereSegments.push(this.uiSegmentSrv.newKeyValue(constraint.value)); - } - - var count = this.whereSegments.length; - var lastSegment = this.whereSegments[Math.max(count - 1, 0)]; - - if (!lastSegment || lastSegment.type !== 'plus-button') { - this.whereSegments.push(this.uiSegmentSrv.newPlusButton()); - } + this.whereSegments.push(sqlPart.create({ type: 'expression', params: ['value', '=', 'value'] })); + // for (let constraint of this.target.where) { + // + // this.whereSegments.push(sqlPart.create({type: 'column',params: ['1']})); + // if (constraint.condition) { + // this.whereSegments.push(this.uiSegmentSrv.newCondition(constraint.condition)); + // } + // this.whereSegments.push(this.uiSegmentSrv.newKey(constraint.key)); + // this.whereSegments.push(this.uiSegmentSrv.newOperator(constraint.operator)); + // this.whereSegments.push(this.uiSegmentSrv.newKeyValue(constraint.value)); + // } } - getWhereSegments(segment, index) { - var query, addTemplateVars; - - if (segment.type === 'condition') { - return this.$q.when([this.uiSegmentSrv.newSegment('AND'), this.uiSegmentSrv.newSegment('OR')]); - } - if (segment.type === 'operator') { - var columnName = this.whereSegments[index - 1].value; - query = this.queryBuilder.buildDatatypeQuery(columnName); - return this.datasource.metricFindQuery(query) - .then(results => { - var datatype = results[0].text; - switch (datatype) { - case "text": - case "character": - case "character varying": - return this.$q.when(this.uiSegmentSrv.newOperators(['=', '!=', '~', '~*','!~','!~*','IN'])); + handleWherePartEvent(whereParts, part, evt, index) { + switch (evt.name) { + case 'get-param-options': { + switch (evt.param.name) { + case 'left': + return this.datasource + .metricFindQuery(this.queryBuilder.buildColumnQuery()) + .then(this.transformToSegments(false)) + .catch(this.handleQueryError.bind(this)); + case 'right': + return this.datasource + .metricFindQuery(this.queryBuilder.buildValueQuery(part.params[0])) + .then(this.transformToSegments(true)) + .catch(this.handleQueryError.bind(this)); + case 'op': + return this.$q.when(this.uiSegmentSrv.newOperators(['=', '!=', '<', '<=', '>', '>=', 'IN'])); default: - return this.$q.when(this.uiSegmentSrv.newOperators(['=', '!=', '<', '<=', '>', '>='])); + return Promise.resolve([]); } - }) - .catch(this.handleQueryError.bind(this)); + } + case 'part-param-changed': { + this.panelCtrl.refresh(); + break; + } + case 'action': { + whereParts.splice(whereParts.indexOf(part), 1); + this.panelCtrl.refresh(); + break; + } + case 'get-part-actions': { + return this.$q.when([{ text: 'Remove', value: 'remove-part' }]); + } } - - if (segment.type === 'key' || segment.type === 'plus-button') { - query = this.queryBuilder.buildColumnQuery(); - - addTemplateVars = false; - } else if (segment.type === 'value') { - query = this.queryBuilder.buildValueQuery(this.whereSegments[index -2].value); - addTemplateVars = true; - } - - return this.datasource - .metricFindQuery(query) - .then(this.transformToSegments(addTemplateVars)) - .then(results => { - if (segment.type === 'key') { - results.splice(0, 0, angular.copy(this.removeWhereFilterSegment)); - } - return results; - }) - .catch(this.handleQueryError.bind(this)); } - whereSegmentUpdated(segment, index) { - this.whereSegments[index] = segment; + getWhereOptions() { + var options = []; + options.push(this.uiSegmentSrv.newSegment({ type: 'function', value: '$__timeFilter' })); + options.push(this.uiSegmentSrv.newSegment({ type: 'function', value: '$__unixEpochFilter' })); + options.push(this.uiSegmentSrv.newSegment({ type: 'function', value: 'Expression' })); + return Promise.resolve(options); + } - // handle remove where condition - if (segment.value === this.removeWhereFilterSegment.value) { - this.whereSegments.splice(index, 3); - if (this.whereSegments.length === 0) { - this.whereSegments.push(this.uiSegmentSrv.newPlusButton()); - } else if (this.whereSegments.length > 2) { - this.whereSegments.splice(Math.max(index - 1, 0), 1); - if (this.whereSegments[this.whereSegments.length - 1].type !== 'plus-button') { - this.whereSegments.push(this.uiSegmentSrv.newPlusButton()); - } + whereAddAction(part, index) { + switch (this.whereAdd.type) { + case 'macro': { + this.whereSegments.push( + sqlPart.create({ type: 'function', name: this.whereAdd.value, params: ['value', '=', 'value'] }) + ); } - } else { - if (segment.type === 'plus-button') { - if (index > 2) { - this.whereSegments.splice(index, 0, this.uiSegmentSrv.newCondition('AND')); - } - this.whereSegments.push(this.uiSegmentSrv.newOperator('=')); - this.whereSegments.push(this.uiSegmentSrv.newFake('select value', 'value', 'query-segment-value')); - segment.type = 'key'; - segment.cssClass = 'query-segment-key'; - } - - if (index + 1 === this.whereSegments.length) { - this.whereSegments.push(this.uiSegmentSrv.newPlusButton()); + default: { + this.whereSegments.push(sqlPart.create({ type: 'expression', params: ['value', '=', 'value'] })); } } - this.rebuildTargetWhereConditions(); - } - - rebuildTargetWhereConditions() { - var where = []; - var tagIndex = 0; - - _.each(this.whereSegments, (segment2, index) => { - if (segment2.type === 'key') { - if (where.length === 0) { - where.push({}); - } - where[tagIndex].key = segment2.value; - } else if (segment2.type === 'value') { - where[tagIndex].value = segment2.value; - } else if (segment2.type === 'template') { - where[tagIndex].value = segment2.value; - } else if (segment2.type === 'condition') { - where.push({ condition: segment2.value }); - tagIndex += 1; - } else if (segment2.type === 'operator') { - where[tagIndex].operator = segment2.value; - } - }); - - this.target.where = where; + this.whereAdd = this.uiSegmentSrv.newPlusButton(); this.panelCtrl.refresh(); } @@ -406,5 +351,4 @@ export class PostgresQueryCtrl extends QueryCtrl { this.error = err.message || 'Failed to issue metric query'; return []; } - } diff --git a/public/app/plugins/datasource/postgres/query_part.ts b/public/app/plugins/datasource/postgres/query_part.ts index d672328c93e..4cda0d47d0d 100644 --- a/public/app/plugins/datasource/postgres/query_part.ts +++ b/public/app/plugins/datasource/postgres/query_part.ts @@ -32,7 +32,7 @@ function replaceAggregationAddStrategy(selectParts, partModel) { // look for existing aggregation for (var i = 0; i < selectParts.length; i++) { var part = selectParts[i]; - if (part.def.type === "aggregate") { + if (part.def.type === 'aggregate') { selectParts[i] = partModel; return; } @@ -83,6 +83,15 @@ function addColumnStrategy(selectParts, partModel, query) { query.selectModels.push(parts); } +function addExpressionStrategy(selectParts, partModel, query) { + // copy all parts + var parts = _.map(selectParts, function(part: any) { + return createPart({ type: part.def.type, params: _.clone(part.params) }); + }); + + query.selectModels.push(parts); +} + register({ type: 'column', style: 'label', @@ -92,11 +101,25 @@ register({ renderer: columnRenderer, }); +register({ + type: 'expression', + style: 'expression', + label: 'Expr:', + addStrategy: addExpressionStrategy, + params: [ + { name: 'left', type: 'string', dynamicLookup: true }, + { name: 'op', type: 'string', dynamicLookup: true }, + { name: 'right', type: 'string', dynamicLookup: true }, + ], + defaultParams: ['value', '=', 'value'], + renderer: columnRenderer, +}); + register({ type: 'aggregate', style: 'label', addStrategy: replaceAggregationAddStrategy, - params: [{name: 'name', type: 'string', dynamicLookup: true}], + params: [{ name: 'name', type: 'string', dynamicLookup: true }], defaultParams: ['avg'], renderer: aggregateRenderer, }); @@ -136,7 +159,7 @@ register({ options: ['none', 'NULL', '0'], }, ], - defaultParams: ['$__interval','none'], + defaultParams: ['$__interval', 'none'], renderer: functionRenderer, }); From 66c56e7bbebd188a232ae6ac7a444c6463a7e9ef Mon Sep 17 00:00:00 2001 From: bergquist Date: Sat, 30 Jun 2018 23:14:18 +0200 Subject: [PATCH 079/786] notifications: dont return error if one notifer failed --- pkg/services/alerting/notifier.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/services/alerting/notifier.go b/pkg/services/alerting/notifier.go index fb2933f6e26..41c4e79445c 100644 --- a/pkg/services/alerting/notifier.go +++ b/pkg/services/alerting/notifier.go @@ -90,7 +90,7 @@ func (n *notificationService) sendNotifications(evalContext *EvalContext, notifi }) if err != nil { - return err + n.log.Error("failed to send notification", "id", not.GetNotifierId()) } } From 9847c2186f551b317fb24a91653d047ede1bc167 Mon Sep 17 00:00:00 2001 From: Sven Klemm Date: Wed, 4 Jul 2018 11:39:58 +0200 Subject: [PATCH 080/786] mv query_part to sql_part --- .../plugins/datasource/postgres/{query_part.ts => sql_part.ts} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename public/app/plugins/datasource/postgres/{query_part.ts => sql_part.ts} (100%) diff --git a/public/app/plugins/datasource/postgres/query_part.ts b/public/app/plugins/datasource/postgres/sql_part.ts similarity index 100% rename from public/app/plugins/datasource/postgres/query_part.ts rename to public/app/plugins/datasource/postgres/sql_part.ts From ced0a5828f0f5f08b84c01bdbc7e25cd2d963983 Mon Sep 17 00:00:00 2001 From: Sven Klemm Date: Wed, 4 Jul 2018 11:56:26 +0200 Subject: [PATCH 081/786] rearrange elements of query builder --- .../postgres/partials/query.editor.html | 46 ++++++++++--------- 1 file changed, 24 insertions(+), 22 deletions(-) diff --git a/public/app/plugins/datasource/postgres/partials/query.editor.html b/public/app/plugins/datasource/postgres/partials/query.editor.html index ea5b4b4e184..4f7bed3e2c4 100644 --- a/public/app/plugins/datasource/postgres/partials/query.editor.html +++ b/public/app/plugins/datasource/postgres/partials/query.editor.html @@ -12,31 +12,13 @@
- + - -
-
- - -
- -
- -
- -
-
-
- -
- -
-
+
@@ -49,7 +31,7 @@
-
@@ -73,7 +55,27 @@
-
+ +
+ + +
+ +
+ +
+ +
+
+
+ +
+ +
+
+ From fee36b2b3530eccd3821d41b0a51e0b3046b2544 Mon Sep 17 00:00:00 2001 From: Sven Klemm Date: Wed, 4 Jul 2018 12:22:45 +0200 Subject: [PATCH 082/786] include where constraints in query generation --- .../datasource/postgres/postgres_query.ts | 60 +++++++++---------- .../plugins/datasource/postgres/query_ctrl.ts | 28 +++------ 2 files changed, 35 insertions(+), 53 deletions(-) diff --git a/public/app/plugins/datasource/postgres/postgres_query.ts b/public/app/plugins/datasource/postgres/postgres_query.ts index cdb51c00561..4e021d37591 100644 --- a/public/app/plugins/datasource/postgres/postgres_query.ts +++ b/public/app/plugins/datasource/postgres/postgres_query.ts @@ -1,11 +1,12 @@ import _ from 'lodash'; -import queryPart from './query_part'; +import sqlPart from './sql_part'; export default class PostgresQuery { target: any; selectModels: any[]; queryBuilder: any; - groupByParts: any; + groupByParts: any[]; + whereParts: any[]; templateSrv: any; scopedVars: any; @@ -31,18 +32,19 @@ export default class PostgresQuery { } quoteIdentifier(value) { - return '"' + value.replace('"','""') + '"'; + return '"' + value.replace('"', '""') + '"'; } quoteLiteral(value) { - return "'" + value.replace("'","''") + "'"; + return "'" + value.replace("'", "''") + "'"; } updateProjection() { this.selectModels = _.map(this.target.select, function(parts: any) { - return _.map(parts, queryPart.create); + return _.map(parts, sqlPart.create); }); - this.groupByParts = _.map(this.target.groupBy, queryPart.create); + this.whereParts = _.map(this.target.where, sqlPart.create); + this.groupByParts = _.map(this.target.groupBy, sqlPart.create); } updatePersistedParts() { @@ -51,6 +53,9 @@ export default class PostgresQuery { return { type: part.def.type, params: part.params }; }); }); + this.target.where = _.map(this.whereParts, function(part: any) { + return { type: part.def.type, params: part.params }; + }); } hasGroupByTime() { @@ -60,8 +65,8 @@ export default class PostgresQuery { addGroupBy(value) { var stringParts = value.match(/^(\w+)(\((.*)\))?$/); var typePart = stringParts[1]; - var args = stringParts[3].split(","); - var partModel = queryPart.create({ type: typePart, params: args }); + var args = stringParts[3].split(','); + var partModel = sqlPart.create({ type: typePart, params: args }); var partCount = this.target.groupBy.length; if (partCount === 0) { @@ -86,7 +91,7 @@ export default class PostgresQuery { // remove aggregations this.target.select = _.map(this.target.select, (s: any) => { return _.filter(s, (part: any) => { - if (part.type === "aggregate") { + if (part.type === 'aggregate') { return false; } return true; @@ -118,25 +123,15 @@ export default class PostgresQuery { this.updatePersistedParts(); } - addSelectPart(selectParts, type) { - var partModel = queryPart.create({ type: type }); - partModel.def.addStrategy(selectParts, partModel, this); - this.updatePersistedParts(); + removeWherePart(whereParts, part) { + var partIndex = _.indexOf(whereParts, part); + whereParts.splice(partIndex, 1); } - private renderWhereConstraint(constraint, index, interpolate) { - var str = ''; - var operator = constraint.operator; - var value = constraint.value; - if (index > 0) { - str = (constraint.condition || 'AND') + ' '; - } - - if (interpolate) { - value = this.templateSrv.replace(value, this.scopedVars); - } - - return str + constraint.key + ' ' + operator + ' ' + value; + addSelectPart(selectParts, type) { + var partModel = sqlPart.create({ type: type }); + partModel.def.addStrategy(selectParts, partModel, this); + this.updatePersistedParts(); } interpolateQueryStr(value, variable, defaultFormatFn) { @@ -170,8 +165,8 @@ export default class PostgresQuery { if (timeGroup) { var args; - if (timeGroup.params.length > 1 && timeGroup.params[1] !== "none") { - args = timeGroup.params.join(","); + if (timeGroup.params.length > 1 && timeGroup.params[1] !== 'none') { + args = timeGroup.params.join(','); } else { args = timeGroup.params[0]; } @@ -181,7 +176,7 @@ export default class PostgresQuery { } if (this.target.metricColumn !== 'None') { - query += "," + this.quoteIdentifier(this.target.metricColumn) + " AS metric"; + query += ',' + this.quoteIdentifier(this.target.metricColumn) + ' AS metric'; } var i, y; @@ -198,7 +193,7 @@ export default class PostgresQuery { query += ' FROM ' + this.quoteIdentifier(target.schema) + '.' + this.quoteIdentifier(target.table) + ' WHERE '; var conditions = _.map(target.where, (tag, index) => { - return this.renderWhereConstraint(tag, index, false); + return tag.params.join(' '); }); if (conditions.length > 0) { @@ -222,8 +217,8 @@ export default class PostgresQuery { if (groupBySection.length) { query += ' GROUP BY ' + groupBySection; - if (this.target.metricColumn !== "None") { - query += ",2"; + if (this.target.metricColumn !== 'None') { + query += ',2'; } } @@ -235,5 +230,4 @@ export default class PostgresQuery { } return query; } - } diff --git a/public/app/plugins/datasource/postgres/query_ctrl.ts b/public/app/plugins/datasource/postgres/query_ctrl.ts index 1eb8b3ef80a..ecec978ae58 100644 --- a/public/app/plugins/datasource/postgres/query_ctrl.ts +++ b/public/app/plugins/datasource/postgres/query_ctrl.ts @@ -2,7 +2,7 @@ import _ from 'lodash'; import { PostgresQueryBuilder } from './query_builder'; import { QueryCtrl } from 'app/plugins/sdk'; import PostgresQuery from './postgres_query'; -import sqlPart from './query_part'; +import sqlPart from './sql_part'; export interface QueryMeta { sql: string; @@ -29,7 +29,6 @@ export class PostgresQueryCtrl extends QueryCtrl { showHelp: boolean; schemaSegment: any; tableSegment: any; - whereSegments: any; whereAdd: any; timeColumnSegment: any; metricColumnSegment: any; @@ -245,18 +244,8 @@ export class PostgresQueryCtrl extends QueryCtrl { } buildWhereSegments() { - this.whereSegments = []; - this.whereSegments.push(sqlPart.create({ type: 'expression', params: ['value', '=', 'value'] })); - // for (let constraint of this.target.where) { - // - // this.whereSegments.push(sqlPart.create({type: 'column',params: ['1']})); - // if (constraint.condition) { - // this.whereSegments.push(this.uiSegmentSrv.newCondition(constraint.condition)); - // } - // this.whereSegments.push(this.uiSegmentSrv.newKey(constraint.key)); - // this.whereSegments.push(this.uiSegmentSrv.newOperator(constraint.operator)); - // this.whereSegments.push(this.uiSegmentSrv.newKeyValue(constraint.value)); - // } + // this.whereSegments = []; + // this.whereSegments.push(sqlPart.create({ type: 'expression', params: ['value', '=', 'value'] })); } handleWherePartEvent(whereParts, part, evt, index) { @@ -276,7 +265,7 @@ export class PostgresQueryCtrl extends QueryCtrl { case 'op': return this.$q.when(this.uiSegmentSrv.newOperators(['=', '!=', '<', '<=', '>', '>=', 'IN'])); default: - return Promise.resolve([]); + return this.$q.when([]); } } case 'part-param-changed': { @@ -284,7 +273,7 @@ export class PostgresQueryCtrl extends QueryCtrl { break; } case 'action': { - whereParts.splice(whereParts.indexOf(part), 1); + this.queryModel.removeWherePart(part, index); this.panelCtrl.refresh(); break; } @@ -299,21 +288,20 @@ export class PostgresQueryCtrl extends QueryCtrl { options.push(this.uiSegmentSrv.newSegment({ type: 'function', value: '$__timeFilter' })); options.push(this.uiSegmentSrv.newSegment({ type: 'function', value: '$__unixEpochFilter' })); options.push(this.uiSegmentSrv.newSegment({ type: 'function', value: 'Expression' })); - return Promise.resolve(options); + return this.$q.when(options); } whereAddAction(part, index) { switch (this.whereAdd.type) { case 'macro': { - this.whereSegments.push( + this.queryModel.whereParts.push( sqlPart.create({ type: 'function', name: this.whereAdd.value, params: ['value', '=', 'value'] }) ); } default: { - this.whereSegments.push(sqlPart.create({ type: 'expression', params: ['value', '=', 'value'] })); + this.queryModel.whereParts.push(sqlPart.create({ type: 'expression', params: ['value', '=', 'value'] })); } } - this.whereAdd = this.uiSegmentSrv.newPlusButton(); this.panelCtrl.refresh(); } From 3b632510fb1f311b2111d5007e10177be3f882a7 Mon Sep 17 00:00:00 2001 From: Sven Klemm Date: Wed, 4 Jul 2018 15:20:52 +0200 Subject: [PATCH 083/786] code formatting From 7d30ca04dec0a9ba9e434e8359e22a5117c802d3 Mon Sep 17 00:00:00 2001 From: Sven Klemm Date: Wed, 4 Jul 2018 15:37:06 +0200 Subject: [PATCH 084/786] remove dead code from sql_part fix where clause query generation --- .../app/core/components/sql_part/sql_part.ts | 33 +++++-------------- .../datasource/postgres/postgres_query.ts | 5 ++- 2 files changed, 12 insertions(+), 26 deletions(-) diff --git a/public/app/core/components/sql_part/sql_part.ts b/public/app/core/components/sql_part/sql_part.ts index e74d4d860c6..f83158177eb 100644 --- a/public/app/core/components/sql_part/sql_part.ts +++ b/public/app/core/components/sql_part/sql_part.ts @@ -21,14 +21,14 @@ export class SqlPartDef { this.label = this.type[0].toUpperCase() + this.type.substring(1) + ':'; } this.style = options.style; - if (this.style === "function") { - this.wrapOpen = "("; - this.wrapClose = ")"; - this.separator = ", "; + if (this.style === 'function') { + this.wrapOpen = '('; + this.wrapClose = ')'; + this.separator = ', '; } else { - this.wrapOpen = " "; - this.wrapClose = " "; - this.separator = " "; + this.wrapOpen = ' '; + this.wrapClose = ' '; + this.separator = ' '; } this.params = options.params; this.defaultParams = options.defaultParams; @@ -60,25 +60,9 @@ export class SqlPart { return this.def.renderer(this, innerExpr); } - hasMultipleParamsInString(strValue, index) { - if (strValue.indexOf(',') === -1) { - return false; - } - - return this.def.params[index + 1] && this.def.params[index + 1].optional; - } - updateParam(strValue, index) { - // handle optional parameters - // if string contains ',' and next param is optional, split and update both - if (this.hasMultipleParamsInString(strValue, index)) { - _.each(strValue.split(','), (partVal, idx) => { - this.updateParam(partVal.trim(), idx); - }); - return; - } - if (strValue === '' && this.def.params[index].optional) { + // XXX check if this is still required this.params.splice(index, 1); } else { this.params[index] = strValue; @@ -132,4 +116,3 @@ export function suffixRenderer(part, innerExpr) { export function identityRenderer(part, innerExpr) { return part.params[0]; } - diff --git a/public/app/plugins/datasource/postgres/postgres_query.ts b/public/app/plugins/datasource/postgres/postgres_query.ts index 4e021d37591..78429739c97 100644 --- a/public/app/plugins/datasource/postgres/postgres_query.ts +++ b/public/app/plugins/datasource/postgres/postgres_query.ts @@ -56,6 +56,9 @@ export default class PostgresQuery { this.target.where = _.map(this.whereParts, function(part: any) { return { type: part.def.type, params: part.params }; }); + this.target.groupBy = _.map(this.groupByParts, function(part: any) { + return { type: part.def.type, params: part.params }; + }); } hasGroupByTime() { @@ -197,7 +200,7 @@ export default class PostgresQuery { }); if (conditions.length > 0) { - query += '(' + conditions.join(' ') + ') AND '; + query += '(' + conditions.join(' AND ') + ') AND '; } query += '$__timeFilter(' + this.quoteIdentifier(target.timeColumn) + ')'; From 3595436614fac1430a7d5ebd50e21fb0a7fe6b13 Mon Sep 17 00:00:00 2001 From: Sven Klemm Date: Wed, 4 Jul 2018 18:57:55 +0200 Subject: [PATCH 085/786] fix where constraint handling --- public/app/plugins/datasource/postgres/query_ctrl.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/public/app/plugins/datasource/postgres/query_ctrl.ts b/public/app/plugins/datasource/postgres/query_ctrl.ts index ecec978ae58..d2b7631dfdc 100644 --- a/public/app/plugins/datasource/postgres/query_ctrl.ts +++ b/public/app/plugins/datasource/postgres/query_ctrl.ts @@ -303,6 +303,7 @@ export class PostgresQueryCtrl extends QueryCtrl { } } this.whereAdd = this.uiSegmentSrv.newPlusButton(); + this.queryModel.updatePersistedParts(); this.panelCtrl.refresh(); } From c6046510924433e0ba9f2c0d5be8779eb22bd13c Mon Sep 17 00:00:00 2001 From: Sven Klemm Date: Thu, 5 Jul 2018 10:19:22 +0200 Subject: [PATCH 086/786] fix group by ui --- .../postgres/partials/query.editor.html | 2 +- .../datasource/postgres/postgres_query.ts | 25 ++++++------------- .../plugins/datasource/postgres/query_ctrl.ts | 20 +++++++++------ 3 files changed, 21 insertions(+), 26 deletions(-) diff --git a/public/app/plugins/datasource/postgres/partials/query.editor.html b/public/app/plugins/datasource/postgres/partials/query.editor.html index 4f7bed3e2c4..28f8af52930 100644 --- a/public/app/plugins/datasource/postgres/partials/query.editor.html +++ b/public/app/plugins/datasource/postgres/partials/query.editor.html @@ -86,7 +86,7 @@
- +
diff --git a/public/app/plugins/datasource/postgres/postgres_query.ts b/public/app/plugins/datasource/postgres/postgres_query.ts index 78429739c97..aad36cee569 100644 --- a/public/app/plugins/datasource/postgres/postgres_query.ts +++ b/public/app/plugins/datasource/postgres/postgres_query.ts @@ -65,27 +65,23 @@ export default class PostgresQuery { return _.find(this.target.groupBy, (g: any) => g.type === 'time'); } - addGroupBy(value) { - var stringParts = value.match(/^(\w+)(\((.*)\))?$/); - var typePart = stringParts[1]; - var args = stringParts[3].split(','); - var partModel = sqlPart.create({ type: typePart, params: args }); + addGroupBy(partType, value) { + var partModel = sqlPart.create({ type: partType, params: [value] }); var partCount = this.target.groupBy.length; if (partCount === 0) { this.target.groupBy.push(partModel.part); - } else if (typePart === 'time') { + } else if (partType === 'time') { + // put timeGroup at start this.target.groupBy.splice(0, 0, partModel.part); - } else if (typePart === 'column') { - if (this.target.groupBy[partCount - 1].type === 'fill') { - this.target.groupBy.splice(partCount - 1, 0, partModel.part); - } else { - this.target.groupBy.push(partModel.part); - } } else { this.target.groupBy.push(partModel.part); } + if (partType === 'time') { + partModel.part.params = ['1m', 'none']; + } + this.updateProjection(); } @@ -126,11 +122,6 @@ export default class PostgresQuery { this.updatePersistedParts(); } - removeWherePart(whereParts, part) { - var partIndex = _.indexOf(whereParts, part); - whereParts.splice(partIndex, 1); - } - addSelectPart(selectParts, type) { var partModel = sqlPart.create({ type: type }); partModel.def.addStrategy(selectParts, partModel, this); diff --git a/public/app/plugins/datasource/postgres/query_ctrl.ts b/public/app/plugins/datasource/postgres/query_ctrl.ts index d2b7631dfdc..37e5aa13bd0 100644 --- a/public/app/plugins/datasource/postgres/query_ctrl.ts +++ b/public/app/plugins/datasource/postgres/query_ctrl.ts @@ -33,7 +33,7 @@ export class PostgresQueryCtrl extends QueryCtrl { timeColumnSegment: any; metricColumnSegment: any; selectMenu: any; - groupBySegment: any; + groupByAdd: any; /** @ngInject **/ constructor($scope, $injector, private templateSrv, private $q, private uiSegmentSrv) { @@ -68,7 +68,7 @@ export class PostgresQueryCtrl extends QueryCtrl { this.buildSelectMenu(); this.buildWhereSegments(); this.whereAdd = this.uiSegmentSrv.newPlusButton(); - this.groupBySegment = this.uiSegmentSrv.newPlusButton(); + this.groupByAdd = this.uiSegmentSrv.newPlusButton(); this.panelCtrl.events.on('data-received', this.onDataReceived.bind(this), $scope); this.panelCtrl.events.on('data-error', this.onDataError.bind(this), $scope); @@ -273,7 +273,7 @@ export class PostgresQueryCtrl extends QueryCtrl { break; } case 'action': { - this.queryModel.removeWherePart(part, index); + whereParts.splice(index, 1); this.panelCtrl.refresh(); break; } @@ -302,7 +302,11 @@ export class PostgresQueryCtrl extends QueryCtrl { this.queryModel.whereParts.push(sqlPart.create({ type: 'expression', params: ['value', '=', 'value'] })); } } - this.whereAdd = this.uiSegmentSrv.newPlusButton(); + + var plusButton = this.uiSegmentSrv.newPlusButton(); + this.whereAdd.html = plusButton.html; + this.whereAdd.value = plusButton.value; + this.queryModel.updatePersistedParts(); this.panelCtrl.refresh(); } @@ -324,15 +328,15 @@ export class PostgresQueryCtrl extends QueryCtrl { } groupByAction() { - switch (this.groupBySegment.value) { + switch (this.groupByAdd.value) { default: { - this.queryModel.addGroupBy(this.groupBySegment.value); + this.queryModel.addGroupBy(this.groupByAdd.type, this.groupByAdd.value); } } var plusButton = this.uiSegmentSrv.newPlusButton(); - this.groupBySegment.value = plusButton.value; - this.groupBySegment.html = plusButton.html; + this.groupByAdd.html = plusButton.html; + this.groupByAdd.value = plusButton.value; this.panelCtrl.refresh(); } From 3f614e635bbbfca9646bb0720d76916e781fa630 Mon Sep 17 00:00:00 2001 From: Sven Klemm Date: Thu, 5 Jul 2018 11:27:19 +0200 Subject: [PATCH 087/786] do not autoquote identifiers --- .../app/plugins/datasource/postgres/postgres_query.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/public/app/plugins/datasource/postgres/postgres_query.ts b/public/app/plugins/datasource/postgres/postgres_query.ts index aad36cee569..7e6f9432c98 100644 --- a/public/app/plugins/datasource/postgres/postgres_query.ts +++ b/public/app/plugins/datasource/postgres/postgres_query.ts @@ -164,13 +164,13 @@ export default class PostgresQuery { } else { args = timeGroup.params[0]; } - query += '$__timeGroup(' + this.quoteIdentifier(target.timeColumn) + ',' + args + ')'; + query += '$__timeGroup(' + target.timeColumn + ',' + args + ')'; } else { - query += this.quoteIdentifier(target.timeColumn) + ' AS time'; + query += target.timeColumn + ' AS "time"'; } if (this.target.metricColumn !== 'None') { - query += ',' + this.quoteIdentifier(this.target.metricColumn) + ' AS metric'; + query += ',' + this.target.metricColumn + ' AS metric'; } var i, y; @@ -185,7 +185,7 @@ export default class PostgresQuery { query += ', ' + selectText; } - query += ' FROM ' + this.quoteIdentifier(target.schema) + '.' + this.quoteIdentifier(target.table) + ' WHERE '; + query += ' FROM ' + target.schema + '.' + target.table + ' WHERE '; var conditions = _.map(target.where, (tag, index) => { return tag.params.join(' '); }); @@ -194,7 +194,7 @@ export default class PostgresQuery { query += '(' + conditions.join(' AND ') + ') AND '; } - query += '$__timeFilter(' + this.quoteIdentifier(target.timeColumn) + ')'; + query += '$__timeFilter(' + target.timeColumn + ')'; var groupBySection = ''; for (i = 0; i < this.groupByParts.length; i++) { From d8c7756489bd26a5e80c3cef22dd545323cb6308 Mon Sep 17 00:00:00 2001 From: Sven Klemm Date: Thu, 5 Jul 2018 21:36:39 +0200 Subject: [PATCH 088/786] dont autoquote, suggest quoted values if requried --- .../datasource/postgres/postgres_query.ts | 8 ++++ .../datasource/postgres/query_builder.ts | 48 ++++++++++--------- .../plugins/datasource/postgres/sql_part.ts | 2 +- 3 files changed, 34 insertions(+), 24 deletions(-) diff --git a/public/app/plugins/datasource/postgres/postgres_query.ts b/public/app/plugins/datasource/postgres/postgres_query.ts index 7e6f9432c98..9cc8de7b491 100644 --- a/public/app/plugins/datasource/postgres/postgres_query.ts +++ b/public/app/plugins/datasource/postgres/postgres_query.ts @@ -31,6 +31,14 @@ export default class PostgresQuery { this.updateProjection(); } + unquoteIdentifier(value) { + if (value[0] === '"') { + return value.substring(1, value.length - 1).replace('""', '"'); + } else { + return value; + } + } + quoteIdentifier(value) { return '"' + value.replace('"', '""') + '"'; } diff --git a/public/app/plugins/datasource/postgres/query_builder.ts b/public/app/plugins/datasource/postgres/query_builder.ts index dd6f95b550c..843f18b0c17 100644 --- a/public/app/plugins/datasource/postgres/query_builder.ts +++ b/public/app/plugins/datasource/postgres/query_builder.ts @@ -1,35 +1,39 @@ - export class PostgresQueryBuilder { constructor(private target, private queryModel) {} buildSchemaQuery() { - var query = "SELECT schema_name FROM information_schema.schemata WHERE"; + var query = 'SELECT quote_ident(schema_name) FROM information_schema.schemata WHERE'; query += " schema_name NOT LIKE 'pg_%' AND schema_name NOT LIKE '\\_%' AND schema_name <> 'information_schema';"; return query; } buildTableQuery() { - var query = "SELECT table_name FROM information_schema.tables WHERE "; - query += "table_schema = " + this.queryModel.quoteLiteral(this.target.schema); + var query = 'SELECT quote_ident(table_name) FROM information_schema.tables WHERE '; + query += 'table_schema = ' + this.quoteLiteral(this.target.schema); return query; } + quoteLiteral(value) { + return this.queryModel.quoteLiteral(this.queryModel.unquoteIdentifier(value)); + } + buildColumnQuery(type?: string) { - var query = "SELECT column_name FROM information_schema.columns WHERE "; - query += "table_schema = " + this.queryModel.quoteLiteral(this.target.schema); - query += " AND table_name = " + this.queryModel.quoteLiteral(this.target.table); + var query = 'SELECT quote_ident(column_name) FROM information_schema.columns WHERE '; + query += 'table_schema = ' + this.quoteLiteral(this.target.schema); + query += ' AND table_name = ' + this.quoteLiteral(this.target.table); switch (type) { - case "time": { - query += " AND data_type IN ('timestamp without time zone','timestamp with time zone','bigint','integer','double precision','real')"; + case 'time': { + query += + " AND data_type IN ('timestamp without time zone','timestamp with time zone','bigint','integer','double precision','real')"; break; } - case "metric": { + case 'metric': { query += " AND data_type IN ('text','char','varchar')"; break; } - case "value": { + case 'value': { query += " AND data_type IN ('bigint','integer','double precision','real')"; break; } @@ -39,27 +43,25 @@ export class PostgresQueryBuilder { } buildValueQuery(column: string) { - var query = "SELECT DISTINCT quote_literal(" + this.queryModel.quoteIdentifier(column) + ")"; - query += " FROM " + this.queryModel.quoteIdentifier(this.target.schema); - query += "." + this.queryModel.quoteIdentifier(this.target.table); - query += " ORDER BY 1 LIMIT 100"; + var query = 'SELECT DISTINCT quote_literal(' + column + ')'; + query += ' FROM ' + this.target.schema + '.' + this.target.table; + query += ' ORDER BY 1 LIMIT 100'; return query; } buildDatatypeQuery(column: string) { - var query = "SELECT data_type FROM information_schema.columns WHERE "; - query += " table_schema = " + this.queryModel.quoteLiteral(this.target.schema); - query += " AND table_name = " + this.queryModel.quoteLiteral(this.target.table); - query += " AND column_name = " + this.queryModel.quoteLiteral(column); + var query = 'SELECT data_type FROM information_schema.columns WHERE '; + query += ' table_schema = ' + this.quoteLiteral(this.target.schema); + query += ' AND table_name = ' + this.quoteLiteral(this.target.table); + query += ' AND column_name = ' + this.quoteLiteral(column); return query; } buildAggregateQuery() { - var query = "SELECT DISTINCT proname FROM pg_aggregate "; - query += "INNER JOIN pg_proc ON pg_aggregate.aggfnoid = pg_proc.oid "; - query += "INNER JOIN pg_type ON pg_type.oid=pg_proc.prorettype "; + var query = 'SELECT DISTINCT proname FROM pg_aggregate '; + query += 'INNER JOIN pg_proc ON pg_aggregate.aggfnoid = pg_proc.oid '; + query += 'INNER JOIN pg_type ON pg_type.oid=pg_proc.prorettype '; query += "WHERE pronargs=1 AND typname IN ('int8','float8') AND aggkind='n' ORDER BY 1"; return query; } - } diff --git a/public/app/plugins/datasource/postgres/sql_part.ts b/public/app/plugins/datasource/postgres/sql_part.ts index 4cda0d47d0d..bb29fde9fb4 100644 --- a/public/app/plugins/datasource/postgres/sql_part.ts +++ b/public/app/plugins/datasource/postgres/sql_part.ts @@ -25,7 +25,7 @@ function aggregateRenderer(part, innerExpr) { } function columnRenderer(part, innerExpr) { - return '"' + part.params[0] + '"'; + return part.params[0]; } function replaceAggregationAddStrategy(selectParts, partModel) { From 85ab1cfa8f9723c44d8c79f9777a02b0423be2e9 Mon Sep 17 00:00:00 2001 From: Sven Klemm Date: Fri, 6 Jul 2018 09:28:34 +0200 Subject: [PATCH 089/786] fix constraint removal --- public/app/plugins/datasource/postgres/postgres_query.ts | 3 ++- public/app/plugins/datasource/postgres/query_ctrl.ts | 2 ++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/public/app/plugins/datasource/postgres/postgres_query.ts b/public/app/plugins/datasource/postgres/postgres_query.ts index 9cc8de7b491..4d90ad83e3b 100644 --- a/public/app/plugins/datasource/postgres/postgres_query.ts +++ b/public/app/plugins/datasource/postgres/postgres_query.ts @@ -31,8 +31,9 @@ export default class PostgresQuery { this.updateProjection(); } + // remove identifier quoting from identifier to use in metadata queries unquoteIdentifier(value) { - if (value[0] === '"') { + if (value[0] === '"' && value[value.length - 1] === '"') { return value.substring(1, value.length - 1).replace('""', '"'); } else { return value; diff --git a/public/app/plugins/datasource/postgres/query_ctrl.ts b/public/app/plugins/datasource/postgres/query_ctrl.ts index 37e5aa13bd0..4aa2db90c1d 100644 --- a/public/app/plugins/datasource/postgres/query_ctrl.ts +++ b/public/app/plugins/datasource/postgres/query_ctrl.ts @@ -273,7 +273,9 @@ export class PostgresQueryCtrl extends QueryCtrl { break; } case 'action': { + // remove element whereParts.splice(index, 1); + this.queryModel.updatePersistedParts(); this.panelCtrl.refresh(); break; } From 8ed210c8d5a130e46745c177a911b9254d022de1 Mon Sep 17 00:00:00 2001 From: Sven Klemm Date: Fri, 6 Jul 2018 10:38:18 +0200 Subject: [PATCH 090/786] remove dead code, make label more flexible --- .../app/core/components/sql_part/sql_part.ts | 36 ++++++------------- .../components/sql_part/sql_part_editor.ts | 2 +- 2 files changed, 11 insertions(+), 27 deletions(-) diff --git a/public/app/core/components/sql_part/sql_part.ts b/public/app/core/components/sql_part/sql_part.ts index f83158177eb..0cc4c31ffbf 100644 --- a/public/app/core/components/sql_part/sql_part.ts +++ b/public/app/core/components/sql_part/sql_part.ts @@ -42,7 +42,8 @@ export class SqlPart { part: any; def: SqlPartDef; params: any[]; - text: string; + label: string; + name: string; constructor(part: any, def: any) { this.part = part; @@ -51,38 +52,21 @@ export class SqlPart { throw { message: 'Could not find sql part ' + part.type }; } + if (part.name) { + this.name = part.name; + this.label = def.label + ' ' + part.name; + } else { + this.name = ''; + this.label = def.label; + } + part.params = part.params || _.clone(this.def.defaultParams); this.params = part.params; - this.updateText(); } render(innerExpr: string) { return this.def.renderer(this, innerExpr); } - - updateParam(strValue, index) { - if (strValue === '' && this.def.params[index].optional) { - // XXX check if this is still required - this.params.splice(index, 1); - } else { - this.params[index] = strValue; - } - - this.part.params = this.params; - this.updateText(); - } - - updateText() { - if (this.params.length === 0) { - this.text = this.def.type + '()'; - return; - } - - var text = this.def.type + '('; - text += this.params.join(', '); - text += ')'; - this.text = text; - } } export function functionRenderer(part, innerExpr) { diff --git a/public/app/core/components/sql_part/sql_part_editor.ts b/public/app/core/components/sql_part/sql_part_editor.ts index e2222abf18f..56329d58a6c 100644 --- a/public/app/core/components/sql_part/sql_part_editor.ts +++ b/public/app/core/components/sql_part/sql_part_editor.ts @@ -4,7 +4,7 @@ import coreModule from 'app/core/core_module'; var template = ` -
+
-
- +
+
@@ -79,7 +79,7 @@ GROUP BY - diff --git a/public/app/plugins/datasource/postgres/postgres_query.ts b/public/app/plugins/datasource/postgres/postgres_query.ts index f9c44553074..12323ec9fac 100644 --- a/public/app/plugins/datasource/postgres/postgres_query.ts +++ b/public/app/plugins/datasource/postgres/postgres_query.ts @@ -1,12 +1,8 @@ import _ from 'lodash'; -import sqlPart from './sql_part'; export default class PostgresQuery { target: any; - selectModels: any[]; queryBuilder: any; - groupByParts: any[]; - whereParts: any[]; templateSrv: any; scopedVars: any; @@ -38,8 +34,6 @@ export default class PostgresQuery { // give interpolateQueryStr access to this this.interpolateQueryStr = this.interpolateQueryStr.bind(this); - - this.updateProjection(); } // remove identifier quoting from identifier to use in metadata queries @@ -59,100 +53,10 @@ export default class PostgresQuery { return "'" + value.replace("'", "''") + "'"; } - updateProjection() { - this.selectModels = _.map(this.target.select, function(parts: any) { - return _.map(parts, sqlPart.create).filter(n => n); - }); - this.whereParts = _.map(this.target.where, sqlPart.create).filter(n => n); - this.groupByParts = _.map(this.target.groupBy, sqlPart.create).filter(n => n); - } - - updatePersistedParts() { - this.target.select = _.map(this.selectModels, function(selectParts) { - return _.map(selectParts, function(part: any) { - return { type: part.def.type, params: part.params }; - }); - }); - this.target.where = _.map(this.whereParts, function(part: any) { - return { type: part.def.type, name: part.name, params: part.params }; - }); - this.target.groupBy = _.map(this.groupByParts, function(part: any) { - return { type: part.def.type, params: part.params }; - }); - } - hasGroupByTime() { return _.find(this.target.groupBy, (g: any) => g.type === 'time'); } - addGroupBy(partType, value) { - let params = [value]; - if (partType === 'time') { - params = ['1m', 'none']; - } - let partModel = sqlPart.create({ type: partType, params: params }); - - if (partType === 'time') { - // put timeGroup at start - this.groupByParts.splice(0, 0, partModel); - } else { - this.groupByParts(partModel); - } - - // add aggregates when adding group by - for (let i = 0; i < this.selectModels.length; i++) { - var selectParts = this.selectModels[i]; - if (!selectParts.some(part => part.def.type === 'aggregate')) { - let aggregate = sqlPart.create({ type: 'aggregate', params: ['avg'] }); - selectParts.splice(1, 0, aggregate); - if (!selectParts.some(part => part.def.type === 'alias')) { - let alias = sqlPart.create({ type: 'alias', params: [selectParts[0].part.params[0]] }); - selectParts.push(alias); - } - } - } - - this.updatePersistedParts(); - } - - removeGroupByPart(part, index) { - if (part.def.type === 'time') { - // remove aggregations - this.selectModels = _.map(this.selectModels, (s: any) => { - return _.filter(s, (part: any) => { - if (part.def.type === 'aggregate') { - return false; - } - return true; - }); - }); - } - - this.groupByParts.splice(index, 1); - this.updatePersistedParts(); - } - - removeSelectPart(selectParts, part) { - // if we remove the field remove the whole statement - if (part.def.type === 'column') { - if (this.selectModels.length > 1) { - let modelsIndex = _.indexOf(this.selectModels, selectParts); - this.selectModels.splice(modelsIndex, 1); - } - } else { - let partIndex = _.indexOf(selectParts, part); - selectParts.splice(partIndex, 1); - } - - this.updatePersistedParts(); - } - - addSelectPart(selectParts, type) { - let partModel = sqlPart.create({ type: type }); - partModel.def.addStrategy(selectParts, partModel, this); - this.updatePersistedParts(); - } - interpolateQueryStr(value, variable, defaultFormatFn) { // if no multi or include all do not regexEscape if (!variable.multi && !variable.includeAll) { diff --git a/public/app/plugins/datasource/postgres/query_ctrl.ts b/public/app/plugins/datasource/postgres/query_ctrl.ts index e10fe25e4ad..27c3d1e50fa 100644 --- a/public/app/plugins/datasource/postgres/query_ctrl.ts +++ b/public/app/plugins/datasource/postgres/query_ctrl.ts @@ -33,6 +33,9 @@ export class PostgresQueryCtrl extends QueryCtrl { timeColumnSegment: any; metricColumnSegment: any; selectMenu: any; + selectModels: any[]; + groupByParts: any[]; + whereParts: any[]; groupByAdd: any; /** @ngInject **/ @@ -41,6 +44,7 @@ export class PostgresQueryCtrl extends QueryCtrl { this.target = this.target; this.queryModel = new PostgresQuery(this.target, templateSrv, this.panel.scopedVars); this.queryBuilder = new PostgresQueryBuilder(this.target, this.queryModel); + this.updateProjection(); this.formats = [{ text: 'Time series', value: 'time_series' }, { text: 'Table', value: 'table' }]; @@ -66,7 +70,6 @@ export class PostgresQueryCtrl extends QueryCtrl { this.metricColumnSegment = uiSegmentSrv.newSegment(this.target.metricColumn); this.buildSelectMenu(); - this.buildWhereSegments(); this.whereAdd = this.uiSegmentSrv.newPlusButton(); this.groupByAdd = this.uiSegmentSrv.newPlusButton(); @@ -74,10 +77,31 @@ export class PostgresQueryCtrl extends QueryCtrl { this.panelCtrl.events.on('data-error', this.onDataError.bind(this), $scope); } + updateProjection() { + this.selectModels = _.map(this.target.select, function(parts: any) { + return _.map(parts, sqlPart.create).filter(n => n); + }); + this.whereParts = _.map(this.target.where, sqlPart.create).filter(n => n); + this.groupByParts = _.map(this.target.groupBy, sqlPart.create).filter(n => n); + } + + updatePersistedParts() { + this.target.select = _.map(this.selectModels, function(selectParts) { + return _.map(selectParts, function(part: any) { + return { type: part.def.type, params: part.params }; + }); + }); + this.target.where = _.map(this.whereParts, function(part: any) { + return { type: part.def.type, name: part.name, params: part.params }; + }); + this.target.groupBy = _.map(this.groupByParts, function(part: any) { + return { type: part.def.type, params: part.params }; + }); + } + buildSelectMenu() { this.selectMenu = [ { text: 'Aggregate', value: 'aggregate' }, - { text: 'Math', value: 'math' }, { text: 'Special', value: 'special' }, { text: 'Alias', value: 'alias' }, { text: 'Column', value: 'column' }, @@ -88,6 +112,10 @@ export class PostgresQueryCtrl extends QueryCtrl { this.target.rawQuery = !this.target.rawQuery; } + resetPlusButton(button) {} + + // schema functions + getSchemaSegments() { return this.datasource .metricFindQuery(this.queryBuilder.buildSchemaQuery()) @@ -95,6 +123,13 @@ export class PostgresQueryCtrl extends QueryCtrl { .catch(this.handleQueryError.bind(this)); } + schemaChanged() { + this.target.schema = this.schemaSegment.value; + this.panelCtrl.refresh(); + } + + // table functions + getTableSegments() { return this.datasource .metricFindQuery(this.queryBuilder.buildTableQuery()) @@ -121,11 +156,6 @@ export class PostgresQueryCtrl extends QueryCtrl { this.panelCtrl.refresh(); } - schemaChanged() { - this.target.schema = this.schemaSegment.value; - this.panelCtrl.refresh(); - } - timeColumnChanged() { this.target.timeColumn = this.timeColumnSegment.value; this.panelCtrl.refresh(); @@ -193,13 +223,34 @@ export class PostgresQueryCtrl extends QueryCtrl { addSelectPart(selectParts, cat, subitem) { if ('submenu' in cat) { - this.queryModel.addSelectPart(selectParts, subitem.value); + this.addSelectPart2(selectParts, subitem.value); } else { - this.queryModel.addSelectPart(selectParts, cat.value); + this.addSelectPart2(selectParts, cat.value); } this.panelCtrl.refresh(); } + removeSelectPart(selectParts, part) { + // if we remove the field remove the whole statement + if (part.def.type === 'column') { + if (this.selectModels.length > 1) { + let modelsIndex = _.indexOf(this.selectModels, selectParts); + this.selectModels.splice(modelsIndex, 1); + } + } else { + let partIndex = _.indexOf(selectParts, part); + selectParts.splice(partIndex, 1); + } + + this.updatePersistedParts(); + } + + addSelectPart2(selectParts, type) { + let partModel = sqlPart.create({ type: type }); + partModel.def.addStrategy(selectParts, partModel, this); + this.updatePersistedParts(); + } + handleSelectPartEvent(selectParts, part, evt) { switch (evt.name) { case 'get-param-options': { @@ -221,7 +272,7 @@ export class PostgresQueryCtrl extends QueryCtrl { break; } case 'action': { - this.queryModel.removeSelectPart(selectParts, part); + this.removeSelectPart(selectParts, part); this.panelCtrl.refresh(); break; } @@ -244,7 +295,7 @@ export class PostgresQueryCtrl extends QueryCtrl { break; } case 'action': { - this.queryModel.removeGroupByPart(part, index); + this.removeGroupBy(part, index); this.panelCtrl.refresh(); break; } @@ -254,6 +305,53 @@ export class PostgresQueryCtrl extends QueryCtrl { } } + addGroupBy(partType, value) { + let params = [value]; + if (partType === 'time') { + params = ['1m', 'none']; + } + let partModel = sqlPart.create({ type: partType, params: params }); + + if (partType === 'time') { + // put timeGroup at start + this.groupByParts.splice(0, 0, partModel); + } else { + this.groupByParts.push(partModel); + } + + // add aggregates when adding group by + for (let i = 0; i < this.selectModels.length; i++) { + var selectParts = this.selectModels[i]; + if (!selectParts.some(part => part.def.type === 'aggregate')) { + let aggregate = sqlPart.create({ type: 'aggregate', params: ['avg'] }); + selectParts.splice(1, 0, aggregate); + if (!selectParts.some(part => part.def.type === 'alias')) { + let alias = sqlPart.create({ type: 'alias', params: [selectParts[0].part.params[0]] }); + selectParts.push(alias); + } + } + } + + this.updatePersistedParts(); + } + + removeGroupBy(part, index) { + if (part.def.type === 'time') { + // remove aggregations + this.selectModels = _.map(this.selectModels, (s: any) => { + return _.filter(s, (part: any) => { + if (part.def.type === 'aggregate') { + return false; + } + return true; + }); + }); + } + + this.groupByParts.splice(index, 1); + this.updatePersistedParts(); + } + buildWhereSegments() { // this.whereSegments = []; // this.whereSegments.push(sqlPart.create({ type: 'expression', params: ['value', '=', 'value'] })); @@ -286,7 +384,7 @@ export class PostgresQueryCtrl extends QueryCtrl { case 'action': { // remove element whereParts.splice(index, 1); - this.queryModel.updatePersistedParts(); + this.updatePersistedParts(); this.panelCtrl.refresh(); break; } @@ -307,11 +405,11 @@ export class PostgresQueryCtrl extends QueryCtrl { whereAddAction(part, index) { switch (this.whereAdd.type) { case 'macro': { - this.queryModel.whereParts.push(sqlPart.create({ type: 'macro', name: this.whereAdd.value, params: [] })); + this.whereParts.push(sqlPart.create({ type: 'macro', name: this.whereAdd.value, params: [] })); break; } default: { - this.queryModel.whereParts.push(sqlPart.create({ type: 'expression', params: ['value', '=', 'value'] })); + this.whereParts.push(sqlPart.create({ type: 'expression', params: ['value', '=', 'value'] })); } } @@ -319,7 +417,7 @@ export class PostgresQueryCtrl extends QueryCtrl { this.whereAdd.html = plusButton.html; this.whereAdd.value = plusButton.value; - this.queryModel.updatePersistedParts(); + this.updatePersistedParts(); this.panelCtrl.refresh(); } @@ -342,7 +440,7 @@ export class PostgresQueryCtrl extends QueryCtrl { groupByAction() { switch (this.groupByAdd.value) { default: { - this.queryModel.addGroupBy(this.groupByAdd.type, this.groupByAdd.value); + this.addGroupBy(this.groupByAdd.type, this.groupByAdd.value); } } From 844beb660d4cfc0928bc5cda6b97247f3b53e29b Mon Sep 17 00:00:00 2001 From: Sven Klemm Date: Sat, 14 Jul 2018 21:00:06 +0200 Subject: [PATCH 136/786] refactor PostgresQueryCtrl --- .../plugins/datasource/postgres/query_ctrl.ts | 67 +++++++------------ 1 file changed, 24 insertions(+), 43 deletions(-) diff --git a/public/app/plugins/datasource/postgres/query_ctrl.ts b/public/app/plugins/datasource/postgres/query_ctrl.ts index 27c3d1e50fa..19e2f8b3a18 100644 --- a/public/app/plugins/datasource/postgres/query_ctrl.ts +++ b/public/app/plugins/datasource/postgres/query_ctrl.ts @@ -112,9 +112,11 @@ export class PostgresQueryCtrl extends QueryCtrl { this.target.rawQuery = !this.target.rawQuery; } - resetPlusButton(button) {} - - // schema functions + resetPlusButton(button) { + let plusButton = this.uiSegmentSrv.newPlusButton(); + button.html = plusButton.html; + button.value = plusButton.value; + } getSchemaSegments() { return this.datasource @@ -128,8 +130,6 @@ export class PostgresQueryCtrl extends QueryCtrl { this.panelCtrl.refresh(); } - // table functions - getTableSegments() { return this.datasource .metricFindQuery(this.queryBuilder.buildTableQuery()) @@ -137,6 +137,11 @@ export class PostgresQueryCtrl extends QueryCtrl { .catch(this.handleQueryError.bind(this)); } + tableChanged() { + this.target.table = this.tableSegment.value; + this.panelCtrl.refresh(); + } + getTimeColumnSegments() { return this.datasource .metricFindQuery(this.queryBuilder.buildColumnQuery('time')) @@ -144,6 +149,11 @@ export class PostgresQueryCtrl extends QueryCtrl { .catch(this.handleQueryError.bind(this)); } + timeColumnChanged() { + this.target.timeColumn = this.timeColumnSegment.value; + this.panelCtrl.refresh(); + } + getMetricColumnSegments() { return this.datasource .metricFindQuery(this.queryBuilder.buildColumnQuery('metric')) @@ -151,16 +161,6 @@ export class PostgresQueryCtrl extends QueryCtrl { .catch(this.handleQueryError.bind(this)); } - tableChanged() { - this.target.table = this.tableSegment.value; - this.panelCtrl.refresh(); - } - - timeColumnChanged() { - this.target.timeColumn = this.timeColumnSegment.value; - this.panelCtrl.refresh(); - } - metricColumnChanged() { this.target.metricColumn = this.metricColumnSegment.value; this.panelCtrl.refresh(); @@ -188,7 +188,7 @@ export class PostgresQueryCtrl extends QueryCtrl { transformToSegments(config) { return results => { - var segments = _.map(results, segment => { + let segments = _.map(results, segment => { return this.uiSegmentSrv.newSegment({ value: segment.text, expandable: segment.expandable, @@ -197,7 +197,7 @@ export class PostgresQueryCtrl extends QueryCtrl { if (config.addTemplateVars) { for (let variable of this.templateSrv.variables) { - var value; + let value; value = '$' + variable.name; if (config.templateQuoter && variable.multi === false) { value = config.templateQuoter(value); @@ -222,17 +222,15 @@ export class PostgresQueryCtrl extends QueryCtrl { } addSelectPart(selectParts, cat, subitem) { - if ('submenu' in cat) { - this.addSelectPart2(selectParts, subitem.value); - } else { - this.addSelectPart2(selectParts, cat.value); - } + let partModel = sqlPart.create({ type: cat.value }); + partModel.def.addStrategy(selectParts, partModel, this); + this.updatePersistedParts(); this.panelCtrl.refresh(); } removeSelectPart(selectParts, part) { - // if we remove the field remove the whole statement if (part.def.type === 'column') { + // remove all parts of column unless its last column if (this.selectModels.length > 1) { let modelsIndex = _.indexOf(this.selectModels, selectParts); this.selectModels.splice(modelsIndex, 1); @@ -245,12 +243,6 @@ export class PostgresQueryCtrl extends QueryCtrl { this.updatePersistedParts(); } - addSelectPart2(selectParts, type) { - let partModel = sqlPart.create({ type: type }); - partModel.def.addStrategy(selectParts, partModel, this); - this.updatePersistedParts(); - } - handleSelectPartEvent(selectParts, part, evt) { switch (evt.name) { case 'get-param-options': { @@ -320,8 +312,7 @@ export class PostgresQueryCtrl extends QueryCtrl { } // add aggregates when adding group by - for (let i = 0; i < this.selectModels.length; i++) { - var selectParts = this.selectModels[i]; + for (let selectParts of this.selectModels) { if (!selectParts.some(part => part.def.type === 'aggregate')) { let aggregate = sqlPart.create({ type: 'aggregate', params: ['avg'] }); selectParts.splice(1, 0, aggregate); @@ -352,11 +343,6 @@ export class PostgresQueryCtrl extends QueryCtrl { this.updatePersistedParts(); } - buildWhereSegments() { - // this.whereSegments = []; - // this.whereSegments.push(sqlPart.create({ type: 'expression', params: ['value', '=', 'value'] })); - } - handleWherePartEvent(whereParts, part, evt, index) { switch (evt.name) { case 'get-param-options': { @@ -413,11 +399,8 @@ export class PostgresQueryCtrl extends QueryCtrl { } } - var plusButton = this.uiSegmentSrv.newPlusButton(); - this.whereAdd.html = plusButton.html; - this.whereAdd.value = plusButton.value; - this.updatePersistedParts(); + this.resetPlusButton(this.whereAdd); this.panelCtrl.refresh(); } @@ -444,9 +427,7 @@ export class PostgresQueryCtrl extends QueryCtrl { } } - var plusButton = this.uiSegmentSrv.newPlusButton(); - this.groupByAdd.html = plusButton.html; - this.groupByAdd.value = plusButton.value; + this.resetPlusButton(this.groupByAdd); this.panelCtrl.refresh(); } From fa66645b0f0c4cec7d730aa4930c2474c497a387 Mon Sep 17 00:00:00 2001 From: Sven Klemm Date: Sun, 15 Jul 2018 09:36:49 +0200 Subject: [PATCH 137/786] refactor PostgresQuery --- .../datasource/postgres/postgres_query.ts | 64 ++++++++++--------- .../plugins/datasource/postgres/query_ctrl.ts | 2 +- .../postgres/specs/postgres_query.jest.ts | 61 +++++++++--------- 3 files changed, 67 insertions(+), 60 deletions(-) diff --git a/public/app/plugins/datasource/postgres/postgres_query.ts b/public/app/plugins/datasource/postgres/postgres_query.ts index 12323ec9fac..7826b455e99 100644 --- a/public/app/plugins/datasource/postgres/postgres_query.ts +++ b/public/app/plugins/datasource/postgres/postgres_query.ts @@ -15,7 +15,7 @@ export default class PostgresQuery { target.schema = target.schema || 'public'; target.format = target.format || 'time_series'; target.timeColumn = target.timeColumn || 'time'; - target.metricColumn = target.metricColumn || 'None'; + target.metricColumn = target.metricColumn || 'none'; target.groupBy = target.groupBy || []; target.where = target.where || []; @@ -57,6 +57,10 @@ export default class PostgresQuery { return _.find(this.target.groupBy, (g: any) => g.type === 'time'); } + hasMetricColumn() { + return this.target.metricColumn !== 'none'; + } + interpolateQueryStr(value, variable, defaultFormatFn) { // if no multi or include all do not regexEscape if (!variable.multi && !variable.includeAll) { @@ -83,7 +87,7 @@ export default class PostgresQuery { } } - query = this.buildQuery(target); + query = this.buildQuery(); if (interpolate) { query = this.templateSrv.replace(query, this.scopedVars, this.interpolateQueryStr); } @@ -91,7 +95,7 @@ export default class PostgresQuery { return query; } - buildTimeColumn(target) { + buildTimeColumn() { let timeGroup = this.hasGroupByTime(); let query; @@ -102,32 +106,32 @@ export default class PostgresQuery { } else { args = timeGroup.params[0]; } - query = '$__timeGroup(' + target.timeColumn + ',' + args + ')'; + query = '$__timeGroup(' + this.target.timeColumn + ',' + args + ')'; } else { - query = target.timeColumn + ' AS "time"'; + query = this.target.timeColumn + ' AS "time"'; } return query; } - buildMetricColumn(target) { - if (target.metricColumn !== 'None') { - return target.metricColumn + ' AS metric'; + buildMetricColumn() { + if (this.hasMetricColumn()) { + return this.target.metricColumn + ' AS metric'; } return ''; } - buildValueColumns(target) { + buildValueColumns() { let query = ''; - for (let i = 0; i < target.select.length; i++) { - query += ',\n ' + this.buildValueColumn(target, target.select[i]); + for (let column of this.target.select) { + query += ',\n ' + this.buildValueColumn(column); } return query; } - buildValueColumn(target, column) { + buildValueColumn(column) { let query = ''; let columnName = _.find(column, (g: any) => g.type === 'column'); @@ -141,15 +145,15 @@ export default class PostgresQuery { let special = _.find(column, (g: any) => g.type === 'special'); if (special) { let over = ''; - if (target.metricColumn !== 'None') { - over = 'PARTITION BY ' + target.metricColumn; + if (this.hasMetricColumn()) { + over = 'PARTITION BY ' + this.target.metricColumn; } switch (special.params[0]) { case 'increase': query = query + ' - lag(' + query + ') OVER (' + over + ')'; break; case 'rate': - let timeColumn = target.timeColumn; + let timeColumn = this.target.timeColumn; let curr = query; let prev = 'lag(' + curr + ') OVER (' + over + ')'; query = '(CASE WHEN ' + curr + ' >= ' + prev + ' THEN ' + curr + ' - ' + prev + ' ELSE ' + curr + ' END)'; @@ -166,12 +170,12 @@ export default class PostgresQuery { return query; } - buildWhereClause(target) { + buildWhereClause() { let query = ''; - let conditions = _.map(target.where, (tag, index) => { + let conditions = _.map(this.target.where, (tag, index) => { switch (tag.type) { case 'macro': - return tag.name + '(' + target.timeColumn + ')'; + return tag.name + '(' + this.target.timeColumn + ')'; break; case 'expression': return tag.params.join(' '); @@ -186,12 +190,12 @@ export default class PostgresQuery { return query; } - buildGroupByClause(target) { + buildGroupByClause() { let query = ''; let groupBySection = ''; - for (let i = 0; i < target.groupBy.length; i++) { - let part = target.groupBy[i]; + for (let i = 0; i < this.target.groupBy.length; i++) { + let part = this.target.groupBy[i]; if (i > 0) { groupBySection += ', '; } @@ -204,26 +208,26 @@ export default class PostgresQuery { if (groupBySection.length) { query = '\nGROUP BY ' + groupBySection; - if (target.metricColumn !== 'None') { + if (this.hasMetricColumn()) { query += ',2'; } } return query; } - buildQuery(target) { + buildQuery() { let query = 'SELECT'; - query += '\n ' + this.buildTimeColumn(target); - if (target.metricColumn !== 'None') { - query += '\n ' + this.buildMetricColumn(target); + query += '\n ' + this.buildTimeColumn(); + if (this.hasMetricColumn()) { + query += '\n ' + this.buildMetricColumn(); } - query += this.buildValueColumns(target); + query += this.buildValueColumns(); - query += '\nFROM ' + target.schema + '.' + target.table; + query += '\nFROM ' + this.target.schema + '.' + this.target.table; - query += this.buildWhereClause(target); - query += this.buildGroupByClause(target); + query += this.buildWhereClause(); + query += this.buildGroupByClause(); query += '\nORDER BY 1'; diff --git a/public/app/plugins/datasource/postgres/query_ctrl.ts b/public/app/plugins/datasource/postgres/query_ctrl.ts index 19e2f8b3a18..609342423fd 100644 --- a/public/app/plugins/datasource/postgres/query_ctrl.ts +++ b/public/app/plugins/datasource/postgres/query_ctrl.ts @@ -214,7 +214,7 @@ export class PostgresQueryCtrl extends QueryCtrl { } if (config.addNone) { - segments.unshift(this.uiSegmentSrv.newSegment({ type: 'template', value: 'None', expandable: true })); + segments.unshift(this.uiSegmentSrv.newSegment({ type: 'template', value: 'none', expandable: true })); } return segments; diff --git a/public/app/plugins/datasource/postgres/specs/postgres_query.jest.ts b/public/app/plugins/datasource/postgres/specs/postgres_query.jest.ts index ef8fdea951b..9559cd3c350 100644 --- a/public/app/plugins/datasource/postgres/specs/postgres_query.jest.ts +++ b/public/app/plugins/datasource/postgres/specs/postgres_query.jest.ts @@ -19,8 +19,10 @@ describe('PostgresQuery', function() { describe('When generating time column SQL', function() { let query = new PostgresQuery({}, templateSrv); - expect(query.buildTimeColumn({ timeColumn: 'time' })).toBe('time AS "time"'); - expect(query.buildTimeColumn({ timeColumn: '"time"' })).toBe('"time" AS "time"'); + query.target.timeColumn = 'time'; + expect(query.buildTimeColumn()).toBe('time AS "time"'); + query.target.timeColumn = '"time"'; + expect(query.buildTimeColumn()).toBe('"time" AS "time"'); }); describe('When generating time column SQL with group by time', function() { @@ -28,65 +30,66 @@ describe('PostgresQuery', function() { { timeColumn: 'time', groupBy: [{ type: 'time', params: ['5m', 'none'] }] }, templateSrv ); - expect(query.buildTimeColumn(query.target)).toBe('$__timeGroup(time,5m)'); + expect(query.buildTimeColumn()).toBe('$__timeGroup(time,5m)'); query = new PostgresQuery({ timeColumn: 'time', groupBy: [{ type: 'time', params: ['5m', 'NULL'] }] }, templateSrv); - expect(query.buildTimeColumn(query.target)).toBe('$__timeGroup(time,5m,NULL)'); + expect(query.buildTimeColumn()).toBe('$__timeGroup(time,5m,NULL)'); }); describe('When generating metric column SQL', function() { let query = new PostgresQuery({}, templateSrv); - expect(query.buildMetricColumn({ metricColumn: 'host' })).toBe('host AS metric'); - expect(query.buildMetricColumn({ metricColumn: '"host"' })).toBe('"host" AS metric'); + query.target.metricColumn = 'host'; + expect(query.buildMetricColumn()).toBe('host AS metric'); + query.target.metricColumn = '"host"'; + expect(query.buildMetricColumn()).toBe('"host" AS metric'); }); describe('When generating value column SQL', function() { let query = new PostgresQuery({}, templateSrv); let column = [{ type: 'column', params: ['value'] }]; - expect(query.buildValueColumn(query.target, column)).toBe('value'); + expect(query.buildValueColumn(column)).toBe('value'); column = [{ type: 'column', params: ['value'] }, { type: 'alias', params: ['alias'] }]; - expect(query.buildValueColumn(query.target, column)).toBe('value AS "alias"'); + expect(query.buildValueColumn(column)).toBe('value AS "alias"'); column = [ { type: 'column', params: ['v'] }, { type: 'alias', params: ['a'] }, { type: 'aggregate', params: ['max'] }, ]; - expect(query.buildValueColumn(query.target, column)).toBe('max(v) AS "a"'); + expect(query.buildValueColumn(column)).toBe('max(v) AS "a"'); column = [ { type: 'column', params: ['v'] }, { type: 'alias', params: ['a'] }, { type: 'special', params: ['increase'] }, ]; - expect(query.buildValueColumn(query.target, column)).toBe('v - lag(v) OVER () AS "a"'); + expect(query.buildValueColumn(column)).toBe('v - lag(v) OVER () AS "a"'); }); describe('When generating WHERE clause', function() { let query = new PostgresQuery({ where: [] }, templateSrv); - let target; - expect(query.buildWhereClause(query.target)).toBe(''); - target = { where: [{ type: 'macro', name: '$__timeFilter' }], timeColumn: 't' }; - expect(query.buildWhereClause(target)).toBe('\nWHERE\n $__timeFilter(t)'); - target = { where: [{ type: 'expression', params: ['v', '=', '1'] }], timeColumn: 't' }; - expect(query.buildWhereClause(target)).toBe('\nWHERE\n v = 1'); - target = { - where: [{ type: 'macro', name: '$__timeFilter' }, { type: 'expression', params: ['v', '=', '1'] }], - timeColumn: 't', - }; - expect(query.buildWhereClause(target)).toBe('\nWHERE\n $__timeFilter(t) AND\n v = 1'); + expect(query.buildWhereClause()).toBe(''); + + query.target.timeColumn = 't'; + query.target.where = [{ type: 'macro', name: '$__timeFilter' }]; + expect(query.buildWhereClause()).toBe('\nWHERE\n $__timeFilter(t)'); + + query.target.where = [{ type: 'expression', params: ['v', '=', '1'] }]; + expect(query.buildWhereClause()).toBe('\nWHERE\n v = 1'); + + query.target.where = [{ type: 'macro', name: '$__timeFilter' }, { type: 'expression', params: ['v', '=', '1'] }]; + expect(query.buildWhereClause()).toBe('\nWHERE\n $__timeFilter(t) AND\n v = 1'); }); describe('When generating GROUP BY clause', function() { - let query = new PostgresQuery({ groupBy: [] }, templateSrv); - let target; + let query = new PostgresQuery({ groupBy: [], metricColumn: 'none' }, templateSrv); - expect(query.buildGroupByClause(query.target)).toBe(''); - target = { groupBy: [{ type: 'time', params: ['5m'] }], metricColumn: 'None' }; - expect(query.buildGroupByClause(target)).toBe('\nGROUP BY 1'); - target = { groupBy: [{ type: 'time', params: ['5m'] }], metricColumn: 'm' }; - expect(query.buildGroupByClause(target)).toBe('\nGROUP BY 1,2'); + expect(query.buildGroupByClause()).toBe(''); + query.target.groupBy = [{ type: 'time', params: ['5m'] }]; + expect(query.buildGroupByClause()).toBe('\nGROUP BY 1'); + query.target.metricColumn = 'm'; + expect(query.buildGroupByClause()).toBe('\nGROUP BY 1,2'); }); describe('When generating complete statement', function() { @@ -99,6 +102,6 @@ describe('PostgresQuery', function() { let result = 'SELECT\n t AS "time",\n value\nFROM public.table\nORDER BY 1'; let query = new PostgresQuery(target, templateSrv); - expect(query.buildQuery(query.target)).toBe(result); + expect(query.buildQuery()).toBe(result); }); }); From d9648f1fe764cbc898da95fec430cb1212bcf99f Mon Sep 17 00:00:00 2001 From: Sven Klemm Date: Sun, 15 Jul 2018 09:42:24 +0200 Subject: [PATCH 138/786] fix bug in query generation with metricColumn --- public/app/plugins/datasource/postgres/postgres_query.ts | 2 +- .../plugins/datasource/postgres/specs/postgres_query.jest.ts | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/public/app/plugins/datasource/postgres/postgres_query.ts b/public/app/plugins/datasource/postgres/postgres_query.ts index 7826b455e99..5b0bd539abe 100644 --- a/public/app/plugins/datasource/postgres/postgres_query.ts +++ b/public/app/plugins/datasource/postgres/postgres_query.ts @@ -220,7 +220,7 @@ export default class PostgresQuery { query += '\n ' + this.buildTimeColumn(); if (this.hasMetricColumn()) { - query += '\n ' + this.buildMetricColumn(); + query += ',\n ' + this.buildMetricColumn(); } query += this.buildValueColumns(); diff --git a/public/app/plugins/datasource/postgres/specs/postgres_query.jest.ts b/public/app/plugins/datasource/postgres/specs/postgres_query.jest.ts index 9559cd3c350..42f1d5243d5 100644 --- a/public/app/plugins/datasource/postgres/specs/postgres_query.jest.ts +++ b/public/app/plugins/datasource/postgres/specs/postgres_query.jest.ts @@ -103,5 +103,9 @@ describe('PostgresQuery', function() { let query = new PostgresQuery(target, templateSrv); expect(query.buildQuery()).toBe(result); + + query.target.metricColumn = 'm'; + result = 'SELECT\n t AS "time",\n m AS metric,\n value\nFROM public.table\nORDER BY 1'; + expect(query.buildQuery()).toBe(result); }); }); From f48060a1bb9eb8da82d14c872c2502d1f490eef2 Mon Sep 17 00:00:00 2001 From: Sven Klemm Date: Sun, 15 Jul 2018 09:52:32 +0200 Subject: [PATCH 139/786] remove render code from sql_part --- .../app/core/components/sql_part/sql_part.ts | 38 ------------------- .../plugins/datasource/postgres/query_ctrl.ts | 9 +++-- .../plugins/datasource/postgres/sql_part.ts | 23 +---------- 3 files changed, 6 insertions(+), 64 deletions(-) diff --git a/public/app/core/components/sql_part/sql_part.ts b/public/app/core/components/sql_part/sql_part.ts index 5b049237881..1929cfdadfa 100644 --- a/public/app/core/components/sql_part/sql_part.ts +++ b/public/app/core/components/sql_part/sql_part.ts @@ -9,7 +9,6 @@ export class SqlPartDef { wrapOpen: string; wrapClose: string; separator: string; - renderer: any; category: any; addStrategy: any; @@ -32,7 +31,6 @@ export class SqlPartDef { } this.params = options.params; this.defaultParams = options.defaultParams; - this.renderer = options.renderer; this.category = options.category; this.addStrategy = options.addStrategy; } @@ -74,40 +72,4 @@ export class SqlPart { this.part.params = this.params; } - - render(innerExpr: string) { - return this.def.renderer(this, innerExpr); - } -} - -export function functionRenderer(part, innerExpr) { - var str = part.def.type + '('; - var parameters = _.map(part.params, (value, index) => { - var paramType = part.def.params[index]; - if (paramType.type === 'time') { - if (value === 'auto') { - value = '$__interval'; - } - } - if (paramType.quote === 'single') { - return "'" + value + "'"; - } else if (paramType.quote === 'double') { - return '"' + value + '"'; - } - - return value; - }); - - if (innerExpr) { - parameters.unshift(innerExpr); - } - return str + parameters.join(', ') + ')'; -} - -export function suffixRenderer(part, innerExpr) { - return innerExpr + ' ' + part.params[0]; -} - -export function identityRenderer(part, innerExpr) { - return part.params[0]; } diff --git a/public/app/plugins/datasource/postgres/query_ctrl.ts b/public/app/plugins/datasource/postgres/query_ctrl.ts index 609342423fd..0b389ae1877 100644 --- a/public/app/plugins/datasource/postgres/query_ctrl.ts +++ b/public/app/plugins/datasource/postgres/query_ctrl.ts @@ -1,6 +1,7 @@ import _ from 'lodash'; import { PostgresQueryBuilder } from './query_builder'; import { QueryCtrl } from 'app/plugins/sdk'; +import { SqlPart } from 'app/core/components/sql_part/sql_part'; import PostgresQuery from './postgres_query'; import sqlPart from './sql_part'; @@ -32,10 +33,10 @@ export class PostgresQueryCtrl extends QueryCtrl { whereAdd: any; timeColumnSegment: any; metricColumnSegment: any; - selectMenu: any; - selectModels: any[]; - groupByParts: any[]; - whereParts: any[]; + selectMenu: any[]; + selectModels: SqlPart[][]; + groupByParts: SqlPart[][]; + whereParts: SqlPart[][]; groupByAdd: any; /** @ngInject **/ diff --git a/public/app/plugins/datasource/postgres/sql_part.ts b/public/app/plugins/datasource/postgres/sql_part.ts index 3e4664c0caf..b9ecc036cd9 100644 --- a/public/app/plugins/datasource/postgres/sql_part.ts +++ b/public/app/plugins/datasource/postgres/sql_part.ts @@ -1,5 +1,5 @@ import _ from 'lodash'; -import { SqlPartDef, SqlPart, functionRenderer, suffixRenderer } from 'app/core/components/sql_part/sql_part'; +import { SqlPartDef, SqlPart } from 'app/core/components/sql_part/sql_part'; var index = []; @@ -16,18 +16,6 @@ function register(options: any) { index[options.type] = new SqlPartDef(options); } -function aliasRenderer(part, innerExpr) { - return innerExpr + ' AS ' + '"' + part.params[0] + '"'; -} - -function aggregateRenderer(part, innerExpr) { - return part.params[0] + '(' + innerExpr + ')'; -} - -function columnRenderer(part, innerExpr) { - return part.params[0]; -} - function replaceAggregationAddStrategy(selectParts, partModel) { var hasAlias = false; @@ -133,7 +121,6 @@ register({ addStrategy: addColumnStrategy, params: [{ type: 'column', dynamicLookup: true }], defaultParams: ['value'], - renderer: columnRenderer, }); register({ @@ -147,7 +134,6 @@ register({ { name: 'right', type: 'string', dynamicLookup: true }, ], defaultParams: ['value', '=', 'value'], - renderer: columnRenderer, }); register({ @@ -157,7 +143,6 @@ register({ addStrategy: addExpressionStrategy, params: [], defaultParams: [], - renderer: columnRenderer, }); register({ @@ -166,7 +151,6 @@ register({ addStrategy: replaceAggregationAddStrategy, params: [{ name: 'name', type: 'string', dynamicLookup: true }], defaultParams: ['avg'], - renderer: aggregateRenderer, }); register({ @@ -175,7 +159,6 @@ register({ addStrategy: addMathStrategy, params: [{ name: 'expr', type: 'string' }], defaultParams: [' / 100'], - renderer: suffixRenderer, }); register({ @@ -184,8 +167,6 @@ register({ addStrategy: addAliasStrategy, params: [{ name: 'name', type: 'string', quote: 'double' }], defaultParams: ['alias'], - renderMode: 'suffix', - renderer: aliasRenderer, }); register({ @@ -205,7 +186,6 @@ register({ }, ], defaultParams: ['$__interval', 'none'], - renderer: functionRenderer, }); register({ @@ -220,7 +200,6 @@ register({ ], defaultParams: ['increase'], addStrategy: replaceSpecialAddStrategy, - renderer: aggregateRenderer, }); export default { From f85c9c012e214a8a35b2321f51486b76a1c60a28 Mon Sep 17 00:00:00 2001 From: Sven Klemm Date: Sun, 15 Jul 2018 12:26:22 +0200 Subject: [PATCH 140/786] refactor adding sqlPart --- .../app/core/components/sql_part/sql_part.ts | 4 - .../postgres/partials/query.editor.html | 2 +- .../plugins/datasource/postgres/query_ctrl.ts | 41 ++++++- .../plugins/datasource/postgres/sql_part.ts | 113 ------------------ 4 files changed, 39 insertions(+), 121 deletions(-) diff --git a/public/app/core/components/sql_part/sql_part.ts b/public/app/core/components/sql_part/sql_part.ts index 1929cfdadfa..5fc6072fc8c 100644 --- a/public/app/core/components/sql_part/sql_part.ts +++ b/public/app/core/components/sql_part/sql_part.ts @@ -9,8 +9,6 @@ export class SqlPartDef { wrapOpen: string; wrapClose: string; separator: string; - category: any; - addStrategy: any; constructor(options: any) { this.type = options.type; @@ -31,8 +29,6 @@ export class SqlPartDef { } this.params = options.params; this.defaultParams = options.defaultParams; - this.category = options.category; - this.addStrategy = options.addStrategy; } } diff --git a/public/app/plugins/datasource/postgres/partials/query.editor.html b/public/app/plugins/datasource/postgres/partials/query.editor.html index f0e15fc1a50..29952c2106b 100644 --- a/public/app/plugins/datasource/postgres/partials/query.editor.html +++ b/public/app/plugins/datasource/postgres/partials/query.editor.html @@ -44,7 +44,7 @@
diff --git a/public/app/plugins/datasource/postgres/query_ctrl.ts b/public/app/plugins/datasource/postgres/query_ctrl.ts index 0b389ae1877..fbd6907f767 100644 --- a/public/app/plugins/datasource/postgres/query_ctrl.ts +++ b/public/app/plugins/datasource/postgres/query_ctrl.ts @@ -222,9 +222,44 @@ export class PostgresQueryCtrl extends QueryCtrl { }; } - addSelectPart(selectParts, cat, subitem) { - let partModel = sqlPart.create({ type: cat.value }); - partModel.def.addStrategy(selectParts, partModel, this); + addSelectPart(selectParts, item) { + let partModel = sqlPart.create({ type: item.value }); + let addAlias = false; + + switch (item.value) { + case 'column': + let parts = _.map(selectParts, function(part: any) { + return sqlPart.create({ type: part.def.type, params: _.clone(part.params) }); + }); + this.selectModels.push(parts); + break; + case 'aggregate': + case 'special': + let index = _.findIndex(selectParts, (p: any) => p.def.type === item.value); + if (index !== -1) { + selectParts[index] = partModel; + } else { + selectParts.splice(1, 0, partModel); + } + if (!_.find(selectParts, (p: any) => p.def.type === 'alias')) { + addAlias = true; + } + break; + case 'alias': + addAlias = true; + break; + } + + if (addAlias) { + // set initial alias name to column name + partModel = sqlPart.create({ type: 'alias', params: [selectParts[0].params[0]] }); + if (selectParts[selectParts.length - 1].def.type === 'alias') { + selectParts[selectParts.length - 1] = partModel; + } else { + selectParts.push(partModel); + } + } + this.updatePersistedParts(); this.panelCtrl.refresh(); } diff --git a/public/app/plugins/datasource/postgres/sql_part.ts b/public/app/plugins/datasource/postgres/sql_part.ts index b9ecc036cd9..be1be82f6b7 100644 --- a/public/app/plugins/datasource/postgres/sql_part.ts +++ b/public/app/plugins/datasource/postgres/sql_part.ts @@ -16,109 +16,9 @@ function register(options: any) { index[options.type] = new SqlPartDef(options); } -function replaceAggregationAddStrategy(selectParts, partModel) { - var hasAlias = false; - - // look for existing aggregation - for (var i = 0; i < selectParts.length; i++) { - var part = selectParts[i]; - if (part.def.type === 'aggregate') { - selectParts[i] = partModel; - return; - } - if (part.def.type === 'alias') { - hasAlias = true; - } - } - - // add alias if none exists yet - if (!hasAlias) { - var aliasModel = createPart({ type: 'alias', params: [selectParts[0].params[0]] }); - selectParts.push(aliasModel); - } - - selectParts.splice(1, 0, partModel); -} - -function replaceSpecialAddStrategy(selectParts, partModel) { - var hasAlias = false; - - // look for existing aggregation - for (var i = 0; i < selectParts.length; i++) { - var part = selectParts[i]; - if (part.def.type === 'special') { - selectParts[i] = partModel; - return; - } - if (part.def.type === 'alias') { - hasAlias = true; - } - } - - // add alias if none exists yet - if (!hasAlias) { - var aliasModel = createPart({ type: 'alias', params: [selectParts[0].params[0]] }); - selectParts.push(aliasModel); - } - - selectParts.splice(1, 0, partModel); -} - -function addMathStrategy(selectParts, partModel) { - var partCount = selectParts.length; - if (partCount > 0) { - // if last is math, replace it - if (selectParts[partCount - 1].def.type === 'math') { - selectParts[partCount - 1] = partModel; - return; - } - // if next to last is math, replace it - if (partCount > 1 && selectParts[partCount - 2].def.type === 'math') { - selectParts[partCount - 2] = partModel; - return; - } else if (selectParts[partCount - 1].def.type === 'alias') { - // if last is alias add it before - selectParts.splice(partCount - 1, 0, partModel); - return; - } - } - selectParts.push(partModel); -} - -function addAliasStrategy(selectParts, partModel) { - var partCount = selectParts.length; - if (partCount > 0) { - // if last is alias, replace it - if (selectParts[partCount - 1].def.type === 'alias') { - selectParts[partCount - 1] = partModel; - return; - } - } - selectParts.push(partModel); -} - -function addColumnStrategy(selectParts, partModel, query) { - // copy all parts - var parts = _.map(selectParts, function(part: any) { - return createPart({ type: part.def.type, params: _.clone(part.params) }); - }); - - query.selectModels.push(parts); -} - -function addExpressionStrategy(selectParts, partModel, query) { - // copy all parts - var parts = _.map(selectParts, function(part: any) { - return createPart({ type: part.def.type, params: _.clone(part.params) }); - }); - - query.selectModels.push(parts); -} - register({ type: 'column', style: 'label', - addStrategy: addColumnStrategy, params: [{ type: 'column', dynamicLookup: true }], defaultParams: ['value'], }); @@ -127,7 +27,6 @@ register({ type: 'expression', style: 'expression', label: 'Expr:', - addStrategy: addExpressionStrategy, params: [ { name: 'left', type: 'string', dynamicLookup: true }, { name: 'op', type: 'string', dynamicLookup: true }, @@ -140,7 +39,6 @@ register({ type: 'macro', style: 'label', label: 'Macro:', - addStrategy: addExpressionStrategy, params: [], defaultParams: [], }); @@ -148,23 +46,13 @@ register({ register({ type: 'aggregate', style: 'label', - addStrategy: replaceAggregationAddStrategy, params: [{ name: 'name', type: 'string', dynamicLookup: true }], defaultParams: ['avg'], }); -register({ - type: 'math', - style: 'label', - addStrategy: addMathStrategy, - params: [{ name: 'expr', type: 'string' }], - defaultParams: [' / 100'], -}); - register({ type: 'alias', style: 'label', - addStrategy: addAliasStrategy, params: [{ name: 'name', type: 'string', quote: 'double' }], defaultParams: ['alias'], }); @@ -199,7 +87,6 @@ register({ }, ], defaultParams: ['increase'], - addStrategy: replaceSpecialAddStrategy, }); export default { From c3c20ef2e2308248147059cee9239cf4cac45c7b Mon Sep 17 00:00:00 2001 From: Sven Klemm Date: Sun, 15 Jul 2018 12:52:00 +0200 Subject: [PATCH 141/786] remove unused import --- public/app/plugins/datasource/postgres/sql_part.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/public/app/plugins/datasource/postgres/sql_part.ts b/public/app/plugins/datasource/postgres/sql_part.ts index be1be82f6b7..6413587f4d9 100644 --- a/public/app/plugins/datasource/postgres/sql_part.ts +++ b/public/app/plugins/datasource/postgres/sql_part.ts @@ -1,4 +1,3 @@ -import _ from 'lodash'; import { SqlPartDef, SqlPart } from 'app/core/components/sql_part/sql_part'; var index = []; From 6e824e81bf43b7315d62266ac2f896adf1dae442 Mon Sep 17 00:00:00 2001 From: Sven Klemm Date: Sun, 15 Jul 2018 15:14:12 +0200 Subject: [PATCH 142/786] fix rate special function when using group by --- .../components/sql_part/sql_part_editor.ts | 40 +++++++++---------- .../plugins/datasource/postgres/datasource.ts | 6 +-- .../datasource/postgres/postgres_query.ts | 4 ++ .../plugins/datasource/postgres/sql_part.ts | 4 +- 4 files changed, 29 insertions(+), 25 deletions(-) diff --git a/public/app/core/components/sql_part/sql_part_editor.ts b/public/app/core/components/sql_part/sql_part_editor.ts index 1b0210af1e7..f272a6d7254 100644 --- a/public/app/core/components/sql_part/sql_part_editor.ts +++ b/public/app/core/components/sql_part/sql_part_editor.ts @@ -2,7 +2,7 @@ import _ from 'lodash'; import $ from 'jquery'; import coreModule from 'app/core/core_module'; -var template = ` +let template = ` From 43686616a06532f9aebd5b5c4acaebff17a4193c Mon Sep 17 00:00:00 2001 From: Sven Klemm Date: Sun, 15 Jul 2018 22:25:05 +0200 Subject: [PATCH 147/786] add query to find metric table --- .../plugins/datasource/postgres/meta_query.ts | 47 +++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/public/app/plugins/datasource/postgres/meta_query.ts b/public/app/plugins/datasource/postgres/meta_query.ts index bf3100f56f2..df5a61ce3c4 100644 --- a/public/app/plugins/datasource/postgres/meta_query.ts +++ b/public/app/plugins/datasource/postgres/meta_query.ts @@ -6,6 +6,53 @@ export class PostgresMetaQuery { return this.queryModel.quoteLiteral(this.queryModel.unquoteIdentifier(value)); } + findMetricTable() { + // query that returns first table found that has a timestamptz column and a float column + let query = ` +SELECT + table_schema, + table_name, + ( SELECT + column_name + FROM information_schema.columns c + WHERE + c.table_schema = t.table_schema AND + c.table_name = t.table_name AND + udt_name IN ('timestamptz','timestamp') + ORDER BY ordinal_position LIMIT 1 + ) AS time_column, + ( SELECT + column_name + FROM information_schema.columns c + WHERE + c.table_schema = t.table_schema AND + c.table_name = t.table_name AND + udt_name='float8' + ORDER BY ordinal_position LIMIT 1 + ) AS value_column +FROM information_schema.tables t +WHERE + table_schema !~* '^_|^pg_|information_schema' AND + EXISTS + ( SELECT 1 + FROM information_schema.columns c + WHERE + c.table_schema = t.table_schema AND + c.table_name = t.table_name AND + udt_name IN ('timestamptz','timestamp') + ) + ( SELECT 1 + FROM information_schema.columns c + WHERE + c.table_schema = t.table_schema AND + c.table_name = t.table_name AND + udt_name='float8' + ) +LIMIT 1 +;`; + return query; + } + buildSchemaQuery() { let query = 'SELECT quote_ident(schema_name) FROM information_schema.schemata WHERE'; query += " schema_name NOT LIKE 'pg_%' AND schema_name NOT LIKE '\\_%' AND schema_name <> 'information_schema';"; From 7f348f38360963d0fdbb79e35783175a28276c0b Mon Sep 17 00:00:00 2001 From: Sven Klemm Date: Sun, 15 Jul 2018 22:26:52 +0200 Subject: [PATCH 148/786] dont run queries if target has no table set --- public/app/plugins/datasource/postgres/postgres_query.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/public/app/plugins/datasource/postgres/postgres_query.ts b/public/app/plugins/datasource/postgres/postgres_query.ts index 0a172de850d..6bec3f87a85 100644 --- a/public/app/plugins/datasource/postgres/postgres_query.ts +++ b/public/app/plugins/datasource/postgres/postgres_query.ts @@ -17,7 +17,7 @@ export default class PostgresQuery { target.metricColumn = target.metricColumn || 'none'; target.groupBy = target.groupBy || []; - target.where = target.where || []; + target.where = target.where || [{ type: 'macro', params: ['$__timeFilter'] }]; target.select = target.select || [[{ type: 'column', params: ['value'] }]]; // handle pre query gui panels gracefully @@ -77,6 +77,10 @@ export default class PostgresQuery { render(interpolate?) { let target = this.target; + if (!('table' in this.target)) { + return ''; + } + if (!target.rawQuery) { target.rawSql = this.buildQuery(); } From 0e608a08c25eaa5308021564852a8f3607a9f28e Mon Sep 17 00:00:00 2001 From: Sven Klemm Date: Sun, 15 Jul 2018 22:58:25 +0200 Subject: [PATCH 149/786] fix test for query generation --- public/app/plugins/datasource/postgres/postgres_query.ts | 2 +- .../plugins/datasource/postgres/specs/postgres_query.jest.ts | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/public/app/plugins/datasource/postgres/postgres_query.ts b/public/app/plugins/datasource/postgres/postgres_query.ts index 6bec3f87a85..4e60eee9e11 100644 --- a/public/app/plugins/datasource/postgres/postgres_query.ts +++ b/public/app/plugins/datasource/postgres/postgres_query.ts @@ -17,7 +17,7 @@ export default class PostgresQuery { target.metricColumn = target.metricColumn || 'none'; target.groupBy = target.groupBy || []; - target.where = target.where || [{ type: 'macro', params: ['$__timeFilter'] }]; + target.where = target.where || [{ type: 'macro', name: '$__timeFilter', params: [] }]; target.select = target.select || [[{ type: 'column', params: ['value'] }]]; // handle pre query gui panels gracefully diff --git a/public/app/plugins/datasource/postgres/specs/postgres_query.jest.ts b/public/app/plugins/datasource/postgres/specs/postgres_query.jest.ts index 42f1d5243d5..d9b3f46b4bf 100644 --- a/public/app/plugins/datasource/postgres/specs/postgres_query.jest.ts +++ b/public/app/plugins/datasource/postgres/specs/postgres_query.jest.ts @@ -98,6 +98,7 @@ describe('PostgresQuery', function() { schema: 'public', table: 'table', select: [[{ type: 'column', params: ['value'] }]], + where: [], }; let result = 'SELECT\n t AS "time",\n value\nFROM public.table\nORDER BY 1'; let query = new PostgresQuery(target, templateSrv); From 9f0b4e0aa779072d64da581976d74b825afc49fe Mon Sep 17 00:00:00 2001 From: Sven Klemm Date: Wed, 18 Jul 2018 13:29:47 +0200 Subject: [PATCH 150/786] add groupby when adding first aggregate --- public/app/plugins/datasource/postgres/query_ctrl.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/public/app/plugins/datasource/postgres/query_ctrl.ts b/public/app/plugins/datasource/postgres/query_ctrl.ts index a0523e10c7c..68eac1cf34a 100644 --- a/public/app/plugins/datasource/postgres/query_ctrl.ts +++ b/public/app/plugins/datasource/postgres/query_ctrl.ts @@ -234,6 +234,10 @@ export class PostgresQueryCtrl extends QueryCtrl { this.selectModels.push(parts); break; case 'aggregate': + // add group by if no group by yet + if (this.target.groupBy.length === 0) { + this.addGroupBy('time', '1m'); + } case 'special': let index = _.findIndex(selectParts, (p: any) => p.def.type === item.value); if (index !== -1) { From 0b421004ea3a41aa73fba414010ecbe5fa687f20 Mon Sep 17 00:00:00 2001 From: Patrick O'Carroll Date: Fri, 20 Jul 2018 09:59:04 +0200 Subject: [PATCH 151/786] built a component for delete button in tables, instead of using a modal to confirm it now does it in the row of the table, created a sass file for the component, the component uses css transitions for animation --- public/app/containers/Teams/TeamList.tsx | 19 +---- .../components/DeleteButton/DeleteButton.tsx | 78 +++++++++++++++++++ public/sass/_grafana.scss | 1 + public/sass/components/_delete_button.scss | 49 ++++++++++++ 4 files changed, 131 insertions(+), 16 deletions(-) create mode 100644 public/app/core/components/DeleteButton/DeleteButton.tsx create mode 100644 public/sass/components/_delete_button.scss diff --git a/public/app/containers/Teams/TeamList.tsx b/public/app/containers/Teams/TeamList.tsx index 4429764b1cc..475f8762c69 100644 --- a/public/app/containers/Teams/TeamList.tsx +++ b/public/app/containers/Teams/TeamList.tsx @@ -6,6 +6,7 @@ import { NavStore } from 'app/stores/NavStore/NavStore'; import { TeamsStore, ITeam } from 'app/stores/TeamsStore/TeamsStore'; import { BackendSrv } from 'app/core/services/backend_srv'; import appEvents from 'app/core/app_events'; +import DeleteButton from 'app/core/components/DeleteButton/DeleteButton'; interface Props { nav: typeof NavStore.Type; @@ -28,18 +29,6 @@ export class TeamList extends React.Component { } deleteTeam(team: ITeam) { - appEvents.emit('confirm-modal', { - title: 'Delete', - text: 'Are you sure you want to delete Team ' + team.name + '?', - yesText: 'Delete', - icon: 'fa-warning', - onConfirm: () => { - this.deleteTeamConfirmed(team); - }, - }); - } - - deleteTeamConfirmed(team) { this.props.backendSrv.delete('/api/teams/' + team.id).then(this.fetchTeams.bind(this)); } @@ -67,9 +56,7 @@ export class TeamList extends React.Component { {team.memberCount} - this.deleteTeam(team)} className="btn btn-danger btn-small"> - - + this.deleteTeam(team)} /> ); @@ -102,7 +89,7 @@ export class TeamList extends React.Component {
-
+
diff --git a/public/app/core/components/DeleteButton/DeleteButton.tsx b/public/app/core/components/DeleteButton/DeleteButton.tsx new file mode 100644 index 00000000000..61a322b591e --- /dev/null +++ b/public/app/core/components/DeleteButton/DeleteButton.tsx @@ -0,0 +1,78 @@ +import React, { Component } from 'react'; + +export default class DeleteButton extends Component { + state = { + deleteButton: 'delete-button--show', + confirmSpan: 'confirm-delete--removed', + }; + + handleDelete = event => { + if (event) { + event.preventDefault(); + } + + this.setState({ + deleteButton: 'delete-button--hide', + }); + + setTimeout(() => { + this.setState({ + deleteButton: 'delete-button--removed', + }); + }, 100); + + setTimeout(() => { + this.setState({ + confirmSpan: 'confirm-delete--hide', + }); + }, 100); + + setTimeout(() => { + this.setState({ + confirmSpan: 'confirm-delete--show', + }); + }, 150); + }; + + cancelDelete = event => { + event.preventDefault(); + + this.setState({ + confirmSpan: 'confirm-delete--hide', + }); + + setTimeout(() => { + this.setState({ + confirmSpan: 'confirm-delete--removed', + deleteButton: 'delete-button--hide', + }); + }, 140); + + setTimeout(() => { + this.setState({ + deleteButton: 'delete-button--show', + }); + }, 190); + }; + + render() { + const { confirmDelete } = this.props; + return ( + + + + + + + + Cancel + + + Confirm Delete + + + + + ); + } +} diff --git a/public/sass/_grafana.scss b/public/sass/_grafana.scss index 9e3bec267ed..3a72bd45a1a 100644 --- a/public/sass/_grafana.scss +++ b/public/sass/_grafana.scss @@ -93,6 +93,7 @@ @import 'components/form_select_box'; @import 'components/user-picker'; @import 'components/description-picker'; +@import 'components/delete_button'; // PAGES @import 'pages/login'; diff --git a/public/sass/components/_delete_button.scss b/public/sass/components/_delete_button.scss new file mode 100644 index 00000000000..19f32189d81 --- /dev/null +++ b/public/sass/components/_delete_button.scss @@ -0,0 +1,49 @@ +.delete-button-container { + max-width: 24px; + width: 24px; + direction: rtl; + max-height: 38px; + display: block; +} + +.confirm-delete-container { + overflow: hidden; + width: 145px; + display: block; +} + +.delete-button { + &--show { + display: inline-block; + opacity: 1; + transition: opacity 0.1s ease; + } + + &--hide { + display: inline-block; + opacity: 0; + transition: opacity 0.1s ease; + } + &--removed { + display: none; + } +} + +.confirm-delete { + &--show { + display: inline-block; + opacity: 1; + transition: opacity 0.08s ease-out, transform 0.1s ease-out; + transform: translateX(0); + } + + &--hide { + display: inline-block; + opacity: 0; + transition: opacity 0.12s ease-in, transform 0.14s ease-in; + transform: translateX(100px); + } + &--removed { + display: none; + } +} From b8a4b7771ae72660fd16022920265750ce42e073 Mon Sep 17 00:00:00 2001 From: Patrick O'Carroll Date: Fri, 20 Jul 2018 11:09:24 +0200 Subject: [PATCH 152/786] removed import appEvents --- public/app/containers/Teams/TeamList.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/public/app/containers/Teams/TeamList.tsx b/public/app/containers/Teams/TeamList.tsx index 475f8762c69..87d24f8ddd4 100644 --- a/public/app/containers/Teams/TeamList.tsx +++ b/public/app/containers/Teams/TeamList.tsx @@ -5,7 +5,6 @@ import PageHeader from 'app/core/components/PageHeader/PageHeader'; import { NavStore } from 'app/stores/NavStore/NavStore'; import { TeamsStore, ITeam } from 'app/stores/TeamsStore/TeamsStore'; import { BackendSrv } from 'app/core/services/backend_srv'; -import appEvents from 'app/core/app_events'; import DeleteButton from 'app/core/components/DeleteButton/DeleteButton'; interface Props { From a2574ac068e0d6adec9727901784d5ac1cfbc749 Mon Sep 17 00:00:00 2001 From: Kim Christensen Date: Fri, 13 Jul 2018 13:24:56 +0200 Subject: [PATCH 153/786] Support timeFilter in templating for InfluxDB After support for queries in template variables was added to InfluxDB, it can be necessary to added dymanic time constraints. This can now be done changing the variable refresh to "On Time Range Changed" for InfluxDB --- public/app/plugins/datasource/influxdb/datasource.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/public/app/plugins/datasource/influxdb/datasource.ts b/public/app/plugins/datasource/influxdb/datasource.ts index f971ac2f649..b9f2b2e03fb 100644 --- a/public/app/plugins/datasource/influxdb/datasource.ts +++ b/public/app/plugins/datasource/influxdb/datasource.ts @@ -187,6 +187,11 @@ export default class InfluxDatasource { return this.$q.when({ results: [] }); } + if (options && options.range) { + var timeFilter = this.getTimeFilter({ rangeRaw: options.range }); + query = query.replace('$timeFilter', timeFilter); + } + return this._influxRequest('GET', '/query', { q: query, epoch: 'ms' }, options); } From dd81f4381de8e663c17e12595b33b46020c153cf Mon Sep 17 00:00:00 2001 From: Kim Christensen Date: Sat, 21 Jul 2018 02:13:41 +0200 Subject: [PATCH 154/786] Add unit test for InfluxDB datasource --- .../influxdb/specs/datasource.jest.ts | 53 +++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 public/app/plugins/datasource/influxdb/specs/datasource.jest.ts diff --git a/public/app/plugins/datasource/influxdb/specs/datasource.jest.ts b/public/app/plugins/datasource/influxdb/specs/datasource.jest.ts new file mode 100644 index 00000000000..6ccbf843dd5 --- /dev/null +++ b/public/app/plugins/datasource/influxdb/specs/datasource.jest.ts @@ -0,0 +1,53 @@ +import InfluxDatasource from '../datasource'; +import $q from 'q'; +import { TemplateSrvStub } from 'test/specs/helpers'; + +describe('InfluxDataSource', () => { + let ctx: any = { + backendSrv: {}, + $q: $q, + templateSrv: new TemplateSrvStub(), + instanceSettings: { url: 'url', name: 'influxDb', jsonData: {} }, + }; + + beforeEach(function() { + ctx.instanceSettings.url = '/api/datasources/proxy/1'; + ctx.ds = new InfluxDatasource(ctx.instanceSettings, ctx.$q, ctx.backendSrv, ctx.templateSrv); + }); + + describe('When issuing metricFindQuery', () => { + let query = 'SELECT max(value) FROM measurement WHERE $timeFilter'; + let queryOptions: any = { + range: { + from: '2018-01-01 00:00:00', + to: '2018-01-02 00:00:00', + }, + }; + let requestQuery; + + beforeEach(async () => { + ctx.backendSrv.datasourceRequest = function(req) { + requestQuery = req.params.q; + return ctx.$q.when({ + results: [ + { + series: [ + { + name: 'measurement', + columns: ['max'], + values: [[1]], + }, + ], + }, + ], + }); + }; + + await ctx.ds.metricFindQuery(query, queryOptions).then(function(_) {}); + }); + + it('should replace $timefilter', () => { + expect(requestQuery).toMatch('time >= 1514761200000ms and time <= 1514847600000ms'); + }); + }); +}); From 84d7743939c6dac456f25c1ad7103142b21e8136 Mon Sep 17 00:00:00 2001 From: Sven Klemm Date: Sat, 21 Jul 2018 09:57:42 +0200 Subject: [PATCH 155/786] fix pre gui queries shortcircuit --- pkg/tsdb/postgres/postgres.go | 14 +++++++++++++- .../plugins/datasource/postgres/postgres_query.ts | 3 ++- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/pkg/tsdb/postgres/postgres.go b/pkg/tsdb/postgres/postgres.go index fdf09216e51..12270da2a48 100644 --- a/pkg/tsdb/postgres/postgres.go +++ b/pkg/tsdb/postgres/postgres.go @@ -170,6 +170,8 @@ func (e PostgresQueryEndpoint) transformToTimeSeries(query *tsdb.Query, rows *co rowCount := 0 timeIndex := -1 metricIndex := -1 + metricPrefix := false + metricPrefixValue := "" // check columns of resultset: a column named time is mandatory // the first text column is treated as metric name unless a column named metric is present @@ -179,6 +181,10 @@ func (e PostgresQueryEndpoint) transformToTimeSeries(query *tsdb.Query, rows *co timeIndex = i case "metric": metricIndex = i + // use metric column as prefix with multiple value columns + if len(columnNames) > 3 { + metricPrefix = true + } default: if metricIndex == -1 { switch columnTypes[i].DatabaseTypeName() { @@ -234,7 +240,11 @@ func (e PostgresQueryEndpoint) transformToTimeSeries(query *tsdb.Query, rows *co if metricIndex >= 0 { if columnValue, ok := values[metricIndex].(string); ok { - metric = columnValue + if metricPrefix { + metricPrefixValue = columnValue + } else { + metric = columnValue + } } else { return fmt.Errorf("Column metric must be of type char,varchar or text, got: %T %v", values[metricIndex], values[metricIndex]) } @@ -251,6 +261,8 @@ func (e PostgresQueryEndpoint) transformToTimeSeries(query *tsdb.Query, rows *co if metricIndex == -1 { metric = col + } else if metricPrefix { + metric = metricPrefixValue + " " + col } series, exist := pointsBySeries[metric] diff --git a/public/app/plugins/datasource/postgres/postgres_query.ts b/public/app/plugins/datasource/postgres/postgres_query.ts index 4e60eee9e11..2222e512e32 100644 --- a/public/app/plugins/datasource/postgres/postgres_query.ts +++ b/public/app/plugins/datasource/postgres/postgres_query.ts @@ -77,7 +77,8 @@ export default class PostgresQuery { render(interpolate?) { let target = this.target; - if (!('table' in this.target)) { + // new query with no table set yet + if (!this.target.rawQuery && !('table' in this.target)) { return ''; } From 7af9cd7dfc722eda93e6e2449ce29feca05b27f0 Mon Sep 17 00:00:00 2001 From: Sven Klemm Date: Sun, 22 Jul 2018 15:06:59 +0200 Subject: [PATCH 156/786] set explicit order for rate and increase --- .../plugins/datasource/postgres/postgres_query.ts | 12 ++++++++++-- .../datasource/postgres/specs/postgres_query.jest.ts | 2 +- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/public/app/plugins/datasource/postgres/postgres_query.ts b/public/app/plugins/datasource/postgres/postgres_query.ts index 2222e512e32..a16a9e5b458 100644 --- a/public/app/plugins/datasource/postgres/postgres_query.ts +++ b/public/app/plugins/datasource/postgres/postgres_query.ts @@ -136,16 +136,24 @@ export default class PostgresQuery { query = columnName.params[0]; let aggregate = _.find(column, (g: any) => g.type === 'aggregate'); + let special = _.find(column, (g: any) => g.type === 'special'); + if (aggregate) { - query = aggregate.params[0] + '(' + query + ')'; + if (special) { + query = aggregate.params[0] + '(' + query + ' ORDER BY ' + this.target.timeColumn + ')'; + } else { + query = aggregate.params[0] + '(' + query + ')'; + } } - let special = _.find(column, (g: any) => g.type === 'special'); if (special) { let over = ''; if (this.hasMetricColumn()) { over = 'PARTITION BY ' + this.target.metricColumn; } + if (!aggregate) { + over += 'ORDER BY ' + this.target.timeColumn; + } switch (special.params[0]) { case 'increase': query = query + ' - lag(' + query + ') OVER (' + over + ')'; diff --git a/public/app/plugins/datasource/postgres/specs/postgres_query.jest.ts b/public/app/plugins/datasource/postgres/specs/postgres_query.jest.ts index d9b3f46b4bf..659ca94496c 100644 --- a/public/app/plugins/datasource/postgres/specs/postgres_query.jest.ts +++ b/public/app/plugins/datasource/postgres/specs/postgres_query.jest.ts @@ -63,7 +63,7 @@ describe('PostgresQuery', function() { { type: 'alias', params: ['a'] }, { type: 'special', params: ['increase'] }, ]; - expect(query.buildValueColumn(column)).toBe('v - lag(v) OVER () AS "a"'); + expect(query.buildValueColumn(column)).toBe('v - lag(v) OVER (ORDER BY time) AS "a"'); }); describe('When generating WHERE clause', function() { From e1a37cf27502e8a6aa9656ba13c5e1bf767bbf7b Mon Sep 17 00:00:00 2001 From: Sven Klemm Date: Sun, 22 Jul 2018 17:12:30 +0200 Subject: [PATCH 157/786] add order by to metadata queries --- public/app/plugins/datasource/postgres/meta_query.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/public/app/plugins/datasource/postgres/meta_query.ts b/public/app/plugins/datasource/postgres/meta_query.ts index df5a61ce3c4..66ff8867393 100644 --- a/public/app/plugins/datasource/postgres/meta_query.ts +++ b/public/app/plugins/datasource/postgres/meta_query.ts @@ -55,7 +55,7 @@ LIMIT 1 buildSchemaQuery() { let query = 'SELECT quote_ident(schema_name) FROM information_schema.schemata WHERE'; - query += " schema_name NOT LIKE 'pg_%' AND schema_name NOT LIKE '\\_%' AND schema_name <> 'information_schema';"; + query += " schema_name !~* '^pg_|^_|information_schema' ORDER BY schema_name"; return query; } @@ -63,6 +63,7 @@ LIMIT 1 buildTableQuery() { let query = 'SELECT quote_ident(table_name) FROM information_schema.tables WHERE '; query += 'table_schema = ' + this.quoteIdentAsLiteral(this.target.schema); + query += ' ORDER BY table_name'; return query; } @@ -92,6 +93,8 @@ LIMIT 1 } } + query += ' ORDER BY column_name'; + return query; } From b3ebc8609383b94584e3d1f083ae15940044c422 Mon Sep 17 00:00:00 2001 From: Sven Klemm Date: Mon, 23 Jul 2018 07:52:42 +0200 Subject: [PATCH 158/786] fix window function query without group by --- public/app/plugins/datasource/postgres/postgres_query.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/app/plugins/datasource/postgres/postgres_query.ts b/public/app/plugins/datasource/postgres/postgres_query.ts index a16a9e5b458..d0aa8a45841 100644 --- a/public/app/plugins/datasource/postgres/postgres_query.ts +++ b/public/app/plugins/datasource/postgres/postgres_query.ts @@ -152,7 +152,7 @@ export default class PostgresQuery { over = 'PARTITION BY ' + this.target.metricColumn; } if (!aggregate) { - over += 'ORDER BY ' + this.target.timeColumn; + over += ' ORDER BY ' + this.target.timeColumn; } switch (special.params[0]) { case 'increase': From 8c52e2cd5703632b568225c87f311cc27b604e54 Mon Sep 17 00:00:00 2001 From: Kim Christensen Date: Mon, 23 Jul 2018 10:05:46 +0200 Subject: [PATCH 159/786] Fix timezone issues in test --- .../plugins/datasource/influxdb/specs/datasource.jest.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/public/app/plugins/datasource/influxdb/specs/datasource.jest.ts b/public/app/plugins/datasource/influxdb/specs/datasource.jest.ts index 6ccbf843dd5..10974cdad97 100644 --- a/public/app/plugins/datasource/influxdb/specs/datasource.jest.ts +++ b/public/app/plugins/datasource/influxdb/specs/datasource.jest.ts @@ -19,8 +19,8 @@ describe('InfluxDataSource', () => { let query = 'SELECT max(value) FROM measurement WHERE $timeFilter'; let queryOptions: any = { range: { - from: '2018-01-01 00:00:00', - to: '2018-01-02 00:00:00', + from: '2018-01-01T00:00:00Z', + to: '2018-01-02T00:00:00Z', }, }; let requestQuery; @@ -47,7 +47,7 @@ describe('InfluxDataSource', () => { }); it('should replace $timefilter', () => { - expect(requestQuery).toMatch('time >= 1514761200000ms and time <= 1514847600000ms'); + expect(requestQuery).toMatch('time >= 1514764800000ms and time <= 1514851200000ms'); }); }); }); From 816ee82d2695157cbd969f43623ae686b683f08d Mon Sep 17 00:00:00 2001 From: Tobias Skarhed Date: Mon, 23 Jul 2018 15:25:59 +0200 Subject: [PATCH 160/786] Add docs about global variables in query template variables --- docs/sources/features/datasources/prometheus.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/sources/features/datasources/prometheus.md b/docs/sources/features/datasources/prometheus.md index 4ff0baee108..190220fb0f1 100644 --- a/docs/sources/features/datasources/prometheus.md +++ b/docs/sources/features/datasources/prometheus.md @@ -75,6 +75,9 @@ Name | Description For details of *metric names*, *label names* and *label values* are please refer to the [Prometheus documentation](http://prometheus.io/docs/concepts/data_model/#metric-names-and-labels). + +It is possible to use some global template variables in Prometheus query template variables; `$__interval`, `$__interval_ms`, `$__range` and `$__range_ms`, where `$__range` is the dashboard's current time range and `$__range_ms` is the current range in milliseconds. + ### Using variables in queries There are two syntaxes: From 70575c8f7816f90b074d7f65226b70e334786958 Mon Sep 17 00:00:00 2001 From: Tobias Skarhed Date: Mon, 23 Jul 2018 15:34:03 +0200 Subject: [PATCH 161/786] Add templating docs for --- docs/sources/reference/templating.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/sources/reference/templating.md b/docs/sources/reference/templating.md index efe9db61e3d..08a142d3636 100644 --- a/docs/sources/reference/templating.md +++ b/docs/sources/reference/templating.md @@ -273,6 +273,9 @@ The `$__timeFilter` is used in the MySQL data source. This variable is only available in the Singlestat panel and can be used in the prefix or suffix fields on the Options tab. The variable will be replaced with the series name or alias. +### The $__range Variable +Currently only supported for Prometheus data sources. This variable represents the range for the current dashboard. It is calculated by `to - from`. It has a millisecond representation called `$__range_ms`. + ## Repeating Panels Template variables can be very useful to dynamically change your queries across a whole dashboard. If you want From 582652145fa825cfce0a85b827d70f09b2cda45e Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Tue, 24 Jul 2018 19:21:23 +0200 Subject: [PATCH 162/786] minor fixes --- docs/sources/features/datasources/prometheus.md | 6 +++++- docs/sources/reference/templating.md | 3 +++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/docs/sources/features/datasources/prometheus.md b/docs/sources/features/datasources/prometheus.md index 190220fb0f1..0ed9e108df6 100644 --- a/docs/sources/features/datasources/prometheus.md +++ b/docs/sources/features/datasources/prometheus.md @@ -76,7 +76,11 @@ Name | Description For details of *metric names*, *label names* and *label values* are please refer to the [Prometheus documentation](http://prometheus.io/docs/concepts/data_model/#metric-names-and-labels). -It is possible to use some global template variables in Prometheus query template variables; `$__interval`, `$__interval_ms`, `$__range` and `$__range_ms`, where `$__range` is the dashboard's current time range and `$__range_ms` is the current range in milliseconds. +#### Using interval and range variables + +> Support for `$__range` and `$__range_ms` only available from Grafana v5.3 + +It's possible to use some global template variables in Prometheus query template variables; `$__interval`, `$__interval_ms`, `$__range` and `$__range_ms`, where `$__range` is the dashboard's current time range and `$__range_ms` is the current range in milliseconds. ### Using variables in queries diff --git a/docs/sources/reference/templating.md b/docs/sources/reference/templating.md index 08a142d3636..ce1a1299d26 100644 --- a/docs/sources/reference/templating.md +++ b/docs/sources/reference/templating.md @@ -274,6 +274,9 @@ The `$__timeFilter` is used in the MySQL data source. This variable is only available in the Singlestat panel and can be used in the prefix or suffix fields on the Options tab. The variable will be replaced with the series name or alias. ### The $__range Variable + +> Only available in Grafana v5.3+ + Currently only supported for Prometheus data sources. This variable represents the range for the current dashboard. It is calculated by `to - from`. It has a millisecond representation called `$__range_ms`. ## Repeating Panels From f4ab432542383c726d517f7a70000460d69ac4b3 Mon Sep 17 00:00:00 2001 From: Patrick O'Carroll Date: Wed, 25 Jul 2018 10:29:55 +0200 Subject: [PATCH 163/786] added position absolute and some flexbox so I could remov changes in display and setTimeout, added tests and types, did some renaming --- public/app/containers/Teams/TeamList.tsx | 2 +- .../DeleteButton/DeleteButton.jest.tsx | 44 ++++++++++ .../components/DeleteButton/DeleteButton.tsx | 82 ++++++++----------- public/sass/components/_delete_button.scss | 37 +++++---- 4 files changed, 99 insertions(+), 66 deletions(-) create mode 100644 public/app/core/components/DeleteButton/DeleteButton.jest.tsx diff --git a/public/app/containers/Teams/TeamList.tsx b/public/app/containers/Teams/TeamList.tsx index 87d24f8ddd4..b86763d8799 100644 --- a/public/app/containers/Teams/TeamList.tsx +++ b/public/app/containers/Teams/TeamList.tsx @@ -55,7 +55,7 @@ export class TeamList extends React.Component { {team.memberCount} ); diff --git a/public/app/core/components/DeleteButton/DeleteButton.jest.tsx b/public/app/core/components/DeleteButton/DeleteButton.jest.tsx new file mode 100644 index 00000000000..12acadee18a --- /dev/null +++ b/public/app/core/components/DeleteButton/DeleteButton.jest.tsx @@ -0,0 +1,44 @@ +import React from 'react'; +import DeleteButton from './DeleteButton'; +import { shallow } from 'enzyme'; + +describe('DeleteButton', () => { + let wrapper; + let deleted; + + beforeAll(() => { + deleted = false; + + function deleteItem() { + deleted = true; + } + wrapper = shallow( deleteItem()} />); + }); + + it('should show confirm delete when clicked', () => { + expect(wrapper.state().showConfirm).toBe(false); + wrapper.find('.delete-button').simulate('click'); + expect(wrapper.state().showConfirm).toBe(true); + }); + + it('should hide confirm delete when clicked', () => { + wrapper.find('.delete-button').simulate('click'); + expect(wrapper.state().showConfirm).toBe(true); + wrapper + .find('.confirm-delete') + .find('.btn') + .at(0) + .simulate('click'); + expect(wrapper.state().showConfirm).toBe(false); + }); + + it('should show confirm delete when clicked', () => { + expect(deleted).toBe(false); + wrapper + .find('.confirm-delete') + .find('.btn') + .at(1) + .simulate('click'); + expect(deleted).toBe(true); + }); +}); diff --git a/public/app/core/components/DeleteButton/DeleteButton.tsx b/public/app/core/components/DeleteButton/DeleteButton.tsx index 61a322b591e..a83ce6097ad 100644 --- a/public/app/core/components/DeleteButton/DeleteButton.tsx +++ b/public/app/core/components/DeleteButton/DeleteButton.tsx @@ -1,73 +1,61 @@ -import React, { Component } from 'react'; +import React, { PureComponent } from 'react'; -export default class DeleteButton extends Component { - state = { - deleteButton: 'delete-button--show', - confirmSpan: 'confirm-delete--removed', +export interface DeleteButtonProps { + onConfirmDelete(); +} + +export interface DeleteButtonStates { + showConfirm: boolean; +} + +export default class DeleteButton extends PureComponent { + state: DeleteButtonStates = { + showConfirm: false, }; - handleDelete = event => { + onClickDelete = event => { if (event) { event.preventDefault(); } this.setState({ - deleteButton: 'delete-button--hide', + showConfirm: true, }); - - setTimeout(() => { - this.setState({ - deleteButton: 'delete-button--removed', - }); - }, 100); - - setTimeout(() => { - this.setState({ - confirmSpan: 'confirm-delete--hide', - }); - }, 100); - - setTimeout(() => { - this.setState({ - confirmSpan: 'confirm-delete--show', - }); - }, 150); }; - cancelDelete = event => { - event.preventDefault(); - + onClickCancel = event => { + if (event) { + event.preventDefault(); + } this.setState({ - confirmSpan: 'confirm-delete--hide', + showConfirm: false, }); - - setTimeout(() => { - this.setState({ - confirmSpan: 'confirm-delete--removed', - deleteButton: 'delete-button--hide', - }); - }, 140); - - setTimeout(() => { - this.setState({ - deleteButton: 'delete-button--show', - }); - }, 190); }; render() { - const { confirmDelete } = this.props; + const onClickConfirm = this.props.onConfirmDelete; + let showConfirm; + let showDeleteButton; + + if (this.state.showConfirm) { + showConfirm = 'show'; + showDeleteButton = 'hide'; + } else { + showConfirm = 'hide'; + showDeleteButton = 'show'; + } + return ( - + - - + + Cancel - + Confirm Delete diff --git a/public/sass/components/_delete_button.scss b/public/sass/components/_delete_button.scss index 19f32189d81..e56a1181a09 100644 --- a/public/sass/components/_delete_button.scss +++ b/public/sass/components/_delete_button.scss @@ -1,49 +1,50 @@ +// sets a fixed width so that the rest of the table +// isn't affected by the animation .delete-button-container { - max-width: 24px; width: 24px; direction: rtl; - max-height: 38px; - display: block; + display: flex; + align-items: center; } +//this container is used to make sure confirm-delete isn't +//shown outside of table .confirm-delete-container { overflow: hidden; width: 145px; - display: block; + position: absolute; + z-index: 1; } .delete-button { - &--show { - display: inline-block; + position: absolute; + + &.show { opacity: 1; transition: opacity 0.1s ease; + z-index: 2; } - &--hide { - display: inline-block; + &.hide { opacity: 0; transition: opacity 0.1s ease; - } - &--removed { - display: none; + z-index: 0; } } .confirm-delete { - &--show { - display: inline-block; + display: flex; + align-items: flex-start; + + &.show { opacity: 1; transition: opacity 0.08s ease-out, transform 0.1s ease-out; transform: translateX(0); } - &--hide { - display: inline-block; + &.hide { opacity: 0; transition: opacity 0.12s ease-in, transform 0.14s ease-in; transform: translateX(100px); } - &--removed { - display: none; - } } From f3504612062f2bcf43a02c985942d5b70ca52439 Mon Sep 17 00:00:00 2001 From: Tobias Skarhed Date: Wed, 25 Jul 2018 14:52:03 +0200 Subject: [PATCH 164/786] Start conversion --- .../specs/variable_srv_init.jest.ts | 238 ++++++++++++++++++ 1 file changed, 238 insertions(+) create mode 100644 public/app/features/templating/specs/variable_srv_init.jest.ts diff --git a/public/app/features/templating/specs/variable_srv_init.jest.ts b/public/app/features/templating/specs/variable_srv_init.jest.ts new file mode 100644 index 00000000000..218170ae454 --- /dev/null +++ b/public/app/features/templating/specs/variable_srv_init.jest.ts @@ -0,0 +1,238 @@ +//import { describe, beforeEach, it, sinon, expect, angularMocks } from 'test/lib/common'; + +import '../all'; + +import _ from 'lodash'; +// import helpers from 'test/specs/helpers'; +// import { Emitter } from 'app/core/core'; +import { VariableSrv } from '../variable_srv'; +import $q from 'q'; + +describe('VariableSrv init', function() { + let templateSrv = { + init: () => {}, + }; + let $injector = { + instantiate: (vars, model) => { + return new vars(model.model); + }, + }; + let $rootscope = { + $on: () => {}, + }; + + let ctx = { + datasourceSrv: {}, + $location: {}, + dashboard: {}, + }; + + // beforeEach(angularMocks.module('grafana.core')); + // beforeEach(angularMocks.module('grafana.controllers')); + // beforeEach(angularMocks.module('grafana.services')); + // beforeEach( + // angularMocks.module(function($compileProvider) { + // $compileProvider.preAssignBindingsEnabled(true); + // }) + // ); + + // beforeEach(ctx.providePhase(['datasourceSrv', 'timeSrv', 'templateSrv', '$location'])); + // beforeEach( + // angularMocks.inject(($rootScope, $q, $location, $injector) => { + // ctx.$q = $q; + // ctx.$rootScope = $rootScope; + // ctx.$location = $location; + // ctx.variableSrv = $injector.get('variableSrv'); + // ctx.$rootScope.$digest(); + // }) + // ); + + function describeInitScenario(desc, fn) { + describe(desc, function() { + // events: new Emitter(), + var scenario: any = { + urlParams: {}, + setup: setupFn => { + scenario.setupFn = setupFn; + }, + }; + + beforeEach(function() { + scenario.setupFn(); + ctx.variableSrv = new VariableSrv($rootscope, $q, {}, $injector, templateSrv); + ctx.variableSrv.datasource = {}; + ctx.variableSrv.datasource.metricFindQuery = jest.fn(() => Promise.resolve(scenario.queryResult)); + + ctx.variableSrv.datasourceSrv = { + get: () => Promise.resolve(ctx.datasource), + getMetricSources: () => Promise.resolve(scenario.metricSources), + }; + + ctx.variableSrv.$location.search = () => Promise.resolve(scenario.urlParams); + ctx.variableSrv.dashboard = { + templating: { list: scenario.variables }, + // events: new Emitter(), + }; + + ctx.variableSrv.init(ctx.variableSrv.dashboard); + // ctx.$rootScope.$digest(); + + scenario.variables = ctx.variableSrv.variables; + }); + + fn(scenario); + }); + } + + ['query', 'interval', 'custom', 'datasource'].forEach(type => { + describeInitScenario('when setting ' + type + ' variable via url', scenario => { + scenario.setup(() => { + scenario.variables = [ + { + name: 'apps', + type: type, + current: { text: 'test', value: 'test' }, + options: [{ text: 'test', value: 'test' }], + }, + ]; + scenario.urlParams['var-apps'] = 'new'; + scenario.metricSources = []; + }); + + it('should update current value', () => { + expect(scenario.variables[0].current.value).toBe('new'); + expect(scenario.variables[0].current.text).toBe('new'); + }); + }); + }); + + describe('given dependent variables', () => { + var variableList = [ + { + name: 'app', + type: 'query', + query: '', + current: { text: 'app1', value: 'app1' }, + options: [{ text: 'app1', value: 'app1' }], + }, + { + name: 'server', + type: 'query', + refresh: 1, + query: '$app.*', + current: { text: 'server1', value: 'server1' }, + options: [{ text: 'server1', value: 'server1' }], + }, + ]; + + describeInitScenario('when setting parent var from url', scenario => { + scenario.setup(() => { + scenario.variables = _.cloneDeep(variableList); + scenario.urlParams['var-app'] = 'google'; + scenario.queryResult = [{ text: 'google-server1' }, { text: 'google-server2' }]; + }); + + it('should update child variable', () => { + expect(scenario.variables[1].options.length).toBe(2); + expect(scenario.variables[1].current.text).toBe('google-server1'); + }); + + it('should only update it once', () => { + expect(ctx.variableSrv.datasource.metricFindQuery).toHaveBeenCalledTimes(1); + }); + }); + }); + + describeInitScenario('when datasource variable is initialized', scenario => { + scenario.setup(() => { + scenario.variables = [ + { + type: 'datasource', + query: 'graphite', + name: 'test', + current: { value: 'backend4_pee', text: 'backend4_pee' }, + regex: '/pee$/', + }, + ]; + scenario.metricSources = [ + { name: 'backend1', meta: { id: 'influx' } }, + { name: 'backend2_pee', meta: { id: 'graphite' } }, + { name: 'backend3', meta: { id: 'graphite' } }, + { name: 'backend4_pee', meta: { id: 'graphite' } }, + ]; + }); + + it('should update current value', function() { + var variable = ctx.variableSrv.variables[0]; + expect(variable.options.length).toBe(2); + }); + }); + + describeInitScenario('when template variable is present in url multiple times', scenario => { + scenario.setup(() => { + scenario.variables = [ + { + name: 'apps', + type: 'query', + multi: true, + current: { text: 'val1', value: 'val1' }, + options: [ + { text: 'val1', value: 'val1' }, + { text: 'val2', value: 'val2' }, + { text: 'val3', value: 'val3', selected: true }, + ], + }, + ]; + scenario.urlParams['var-apps'] = ['val2', 'val1']; + }); + + it('should update current value', function() { + var variable = ctx.variableSrv.variables[0]; + expect(variable.current.value.length).toBe(2); + expect(variable.current.value[0]).toBe('val2'); + expect(variable.current.value[1]).toBe('val1'); + expect(variable.current.text).toBe('val2 + val1'); + expect(variable.options[0].selected).toBe(true); + expect(variable.options[1].selected).toBe(true); + }); + + it('should set options that are not in value to selected false', function() { + var variable = ctx.variableSrv.variables[0]; + expect(variable.options[2].selected).toBe(false); + }); + }); + + describeInitScenario('when template variable is present in url multiple times using key/values', scenario => { + scenario.setup(() => { + scenario.variables = [ + { + name: 'apps', + type: 'query', + multi: true, + current: { text: 'Val1', value: 'val1' }, + options: [ + { text: 'Val1', value: 'val1' }, + { text: 'Val2', value: 'val2' }, + { text: 'Val3', value: 'val3', selected: true }, + ], + }, + ]; + scenario.urlParams['var-apps'] = ['val2', 'val1']; + }); + + it('should update current value', function() { + var variable = ctx.variableSrv.variables[0]; + expect(variable.current.value.length).toBe(2); + expect(variable.current.value[0]).toBe('val2'); + expect(variable.current.value[1]).toBe('val1'); + expect(variable.current.text).toBe('Val2 + Val1'); + expect(variable.options[0].selected).toBe(true); + expect(variable.options[1].selected).toBe(true); + }); + + it('should set options that are not in value to selected false', function() { + var variable = ctx.variableSrv.variables[0]; + expect(variable.options[2].selected).toBe(false); + }); + }); +}); From 7d51c1524007fc47dc225e1256535c1386c07aca Mon Sep 17 00:00:00 2001 From: Tobias Skarhed Date: Wed, 25 Jul 2018 16:15:03 +0200 Subject: [PATCH 165/786] Two passing tests --- .../specs/variable_srv_init.jest.ts | 57 ++++++++++++++----- .../app/features/templating/variable_srv.ts | 1 + 2 files changed, 43 insertions(+), 15 deletions(-) diff --git a/public/app/features/templating/specs/variable_srv_init.jest.ts b/public/app/features/templating/specs/variable_srv_init.jest.ts index 218170ae454..519adc0a350 100644 --- a/public/app/features/templating/specs/variable_srv_init.jest.ts +++ b/public/app/features/templating/specs/variable_srv_init.jest.ts @@ -7,16 +7,18 @@ import _ from 'lodash'; // import { Emitter } from 'app/core/core'; import { VariableSrv } from '../variable_srv'; import $q from 'q'; +// import { model } from 'mobx-state-tree/dist/internal'; describe('VariableSrv init', function() { let templateSrv = { - init: () => {}, - }; - let $injector = { - instantiate: (vars, model) => { - return new vars(model.model); + init: vars => { + this.variables = vars; }, + variableInitialized: () => {}, + updateTemplateData: () => {}, + replace: str => str, }; + let $injector = {}; let $rootscope = { $on: () => {}, }; @@ -57,24 +59,35 @@ describe('VariableSrv init', function() { }, }; - beforeEach(function() { + beforeEach(async () => { scenario.setupFn(); - ctx.variableSrv = new VariableSrv($rootscope, $q, {}, $injector, templateSrv); - ctx.variableSrv.datasource = {}; - ctx.variableSrv.datasource.metricFindQuery = jest.fn(() => Promise.resolve(scenario.queryResult)); - - ctx.variableSrv.datasourceSrv = { - get: () => Promise.resolve(ctx.datasource), - getMetricSources: () => Promise.resolve(scenario.metricSources), + ctx = { + datasource: { + metricFindQuery: jest.fn(() => Promise.resolve(scenario.queryResult)), + }, + datasourceSrv: { + get: () => Promise.resolve(ctx.datasource), + getMetricSources: () => Promise.resolve(scenario.metricSources), + }, + templateSrv, }; + ctx.variableSrv = new VariableSrv($rootscope, $q, {}, $injector, templateSrv); + + $injector.instantiate = (variable, model) => { + return getVarMockConstructor(variable, model, ctx); + }; + + ctx.variableSrv.datasource = ctx.datasource; + ctx.variableSrv.datasourceSrv = ctx.datasourceSrv; + ctx.variableSrv.$location.search = () => Promise.resolve(scenario.urlParams); ctx.variableSrv.dashboard = { templating: { list: scenario.variables }, - // events: new Emitter(), + // events: new Emitter(), }; - ctx.variableSrv.init(ctx.variableSrv.dashboard); + await ctx.variableSrv.init(ctx.variableSrv.dashboard); // ctx.$rootScope.$digest(); scenario.variables = ctx.variableSrv.variables; @@ -236,3 +249,17 @@ describe('VariableSrv init', function() { }); }); }); + +function getVarMockConstructor(variable, model, ctx) { + console.log(model.model.type); + switch (model.model.type) { + case 'datasource': + return new variable(model.model, ctx.datasourceSrv, ctx.templateSrv, ctx.variableSrv); + case 'query': + return new variable(model.model, ctx.datasourceSrv, ctx.templateSrv, ctx.variableSrv); + case 'interval': + return new variable(model.model, {}, ctx.templateSrv, ctx.variableSrv); + default: + return new variable(model.model); + } +} diff --git a/public/app/features/templating/variable_srv.ts b/public/app/features/templating/variable_srv.ts index 8ad3c2845e2..9f6522c9b86 100644 --- a/public/app/features/templating/variable_srv.ts +++ b/public/app/features/templating/variable_srv.ts @@ -23,6 +23,7 @@ export class VariableSrv { // init variables for (let variable of this.variables) { + console.log(variable); variable.initLock = this.$q.defer(); } From 0f99e624b680b60e00ca05f408c5b85464d7cf81 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Wed, 25 Jul 2018 16:20:00 +0200 Subject: [PATCH 166/786] docs: using interval and range variables in prometheus Included example usages --- .../features/datasources/prometheus.md | 21 ++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/docs/sources/features/datasources/prometheus.md b/docs/sources/features/datasources/prometheus.md index 0ed9e108df6..3a04ef92e31 100644 --- a/docs/sources/features/datasources/prometheus.md +++ b/docs/sources/features/datasources/prometheus.md @@ -80,7 +80,26 @@ For details of *metric names*, *label names* and *label values* are please refer > Support for `$__range` and `$__range_ms` only available from Grafana v5.3 -It's possible to use some global template variables in Prometheus query template variables; `$__interval`, `$__interval_ms`, `$__range` and `$__range_ms`, where `$__range` is the dashboard's current time range and `$__range_ms` is the current range in milliseconds. +It's possible to use some global built-in variables in query variables; `$__interval`, `$__interval_ms`, `$__range` and `$__range_ms`, see [Global built-in variables](/reference/templating/#global-built-in-variables) for more information. These can be convenient to use in conjunction with the `query_result` function when you need to filter variable queries since +`label_values` function doesn't support queries. + +Make sure to set the variable's `refresh` trigger to be `On Time Range Change` to get the correct instances when changing the time range on the dashboard. + +**Example usage:** + +Populate a variable with the the busiest 5 request instances based on average QPS over the time range shown in the dashboard: + +``` +Query: query_result(topk(5, sum(rate(http_requests_total[$__range])) by (instance))) +Regex: /"([^"]+)"/ +``` + +Populate a variable with the instances having a certain state over the time range shown in the dashboard: + +``` +Query: query_result(max_over_time([$__range]) != ) +Regex: +``` ### Using variables in queries From 84e431d377b51405f37b4bae8321454218bcc7c4 Mon Sep 17 00:00:00 2001 From: David Kaltschmidt Date: Wed, 25 Jul 2018 16:16:33 +0200 Subject: [PATCH 167/786] Add tslib to TS compiler - using tslib reduces bundle sizes - add compiler option for easier default imports of CJS modules - remove double entry of fork-ts-checker-plugin - speed up hot reload by using exprimental ts-loader API --- package.json | 16 ++++---- scripts/webpack/webpack.hot.js | 10 ++++- tsconfig.json | 73 +++++++++++++++++++--------------- yarn.lock | 8 +++- 4 files changed, 65 insertions(+), 42 deletions(-) diff --git a/package.json b/package.json index c26438230cc..c0581c1de43 100644 --- a/package.json +++ b/package.json @@ -34,7 +34,7 @@ "expose-loader": "^0.7.3", "extract-text-webpack-plugin": "^4.0.0-beta.0", "file-loader": "^1.1.11", - "fork-ts-checker-webpack-plugin": "^0.4.1", + "fork-ts-checker-webpack-plugin": "^0.4.2", "gaze": "^1.1.2", "glob": "~7.0.0", "grunt": "1.0.1", @@ -71,12 +71,14 @@ "karma-webpack": "^3.0.0", "lint-staged": "^6.0.0", "load-grunt-tasks": "3.5.2", + "mini-css-extract-plugin": "^0.4.0", "mobx-react-devtools": "^4.2.15", "mocha": "^4.0.1", "ng-annotate-loader": "^0.6.1", "ng-annotate-webpack-plugin": "^0.2.1-pre", "ngtemplate-loader": "^2.0.1", "npm": "^5.4.2", + "optimize-css-assets-webpack-plugin": "^4.0.2", "phantomjs-prebuilt": "^2.1.15", "postcss-browser-reporter": "^0.5.0", "postcss-loader": "^2.0.6", @@ -90,15 +92,16 @@ "style-loader": "^0.21.0", "systemjs": "0.20.19", "systemjs-plugin-css": "^0.1.36", - "ts-loader": "^4.3.0", "ts-jest": "^22.4.6", + "ts-loader": "^4.3.0", + "tslib": "^1.9.3", "tslint": "^5.8.0", "tslint-loader": "^3.5.3", "typescript": "^2.6.2", + "uglifyjs-webpack-plugin": "^1.2.7", "webpack": "^4.8.0", "webpack-bundle-analyzer": "^2.9.0", "webpack-cleanup-plugin": "^0.5.1", - "fork-ts-checker-webpack-plugin": "^0.4.2", "webpack-cli": "^2.1.4", "webpack-dev-server": "^3.1.0", "webpack-merge": "^4.1.0", @@ -155,14 +158,12 @@ "immutable": "^3.8.2", "jquery": "^3.2.1", "lodash": "^4.17.10", - "mini-css-extract-plugin": "^0.4.0", "mobx": "^3.4.1", "mobx-react": "^4.3.5", "mobx-state-tree": "^1.3.1", "moment": "^2.22.2", "mousetrap": "^1.6.0", "mousetrap-global-bind": "^1.1.0", - "optimize-css-assets-webpack-plugin": "^4.0.2", "prismjs": "^1.6.0", "prop-types": "^15.6.0", "react": "^16.2.0", @@ -181,10 +182,9 @@ "slate-react": "^0.12.4", "tether": "^1.4.0", "tether-drop": "https://github.com/torkelo/drop/tarball/master", - "tinycolor2": "^1.4.1", - "uglifyjs-webpack-plugin": "^1.2.7" + "tinycolor2": "^1.4.1" }, "resolutions": { "caniuse-db": "1.0.30000772" } -} +} \ No newline at end of file diff --git a/scripts/webpack/webpack.hot.js b/scripts/webpack/webpack.hot.js index 28c8cec504d..0305a6f465c 100644 --- a/scripts/webpack/webpack.hot.js +++ b/scripts/webpack/webpack.hot.js @@ -20,6 +20,7 @@ module.exports = merge(common, { path: path.resolve(__dirname, '../../public/build'), filename: '[name].[hash].js', publicPath: "/public/build/", + pathinfo: false, }, resolve: { @@ -37,6 +38,12 @@ module.exports = merge(common, { } }, + optimization: { + removeAvailableModules: false, + removeEmptyChunks: false, + splitChunks: false, + }, + module: { rules: [ { @@ -56,7 +63,8 @@ module.exports = merge(common, { { loader: 'ts-loader', options: { - transpileOnly: true + transpileOnly: true, + experimentalWatchApi: true }, }], }, diff --git a/tsconfig.json b/tsconfig.json index 3596930a62f..3ef1dd1b769 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,32 +1,43 @@ { - "compilerOptions": { - "moduleResolution": "node", - "outDir": "public/dist", - "target": "es5", - "lib": ["es6", "dom"], - "rootDir": "public/", - "jsx": "react", - "module": "esnext", - "declaration": false, - "allowSyntheticDefaultImports": true, - "inlineSourceMap": false, - "sourceMap": true, - "noEmitOnError": false, - "emitDecoratorMetadata": false, - "experimentalDecorators": true, - "noImplicitReturns": true, - "noImplicitThis": false, - "noImplicitUseStrict":false, - "noImplicitAny": false, - "noUnusedLocals": true, - "baseUrl": "public", - "paths": { - "app": ["app"] - } - }, - "include": [ - "public/app/**/*.ts", - "public/app/**/*.tsx", - "public/test/**/*.ts" - ] -} + "compilerOptions": { + "moduleResolution": "node", + "outDir": "public/dist", + "target": "es5", + "lib": [ + "es6", + "dom" + ], + "rootDir": "public/", + "jsx": "react", + "module": "esnext", + "declaration": false, + "allowSyntheticDefaultImports": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "importHelpers": true, // importing helper functions from tslib + "noEmitHelpers": true, // disable emitting inline helper functions + "removeComments": false, // comments are needed by angular injections + "inlineSourceMap": false, + "sourceMap": true, + "noEmitOnError": false, + "emitDecoratorMetadata": false, + "experimentalDecorators": true, + "noImplicitReturns": true, + "noImplicitThis": false, + "noImplicitUseStrict": false, + "noImplicitAny": false, + "noUnusedLocals": true, + "baseUrl": "public", + "pretty": true, + "paths": { + "app": [ + "app" + ] + } + }, + "include": [ + "public/app/**/*.ts", + "public/app/**/*.tsx", + "public/test/**/*.ts" + ] +} \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index 6772d7c14a4..6e737e33348 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3101,7 +3101,7 @@ d3-request@1.0.6: d3-dsv "1" xmlhttprequest "1" -d3-scale-chromatic@^1.1.1: +d3-scale-chromatic@^1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/d3-scale-chromatic/-/d3-scale-chromatic-1.3.0.tgz#7ee38ffcaa7ad55cfed83a6a668aac5570c653c4" dependencies: @@ -7974,7 +7974,7 @@ mocha@^4.0.1: mkdirp "0.5.1" supports-color "4.4.0" -moment@^2.18.1: +moment@^2.22.2: version "2.22.2" resolved "https://registry.yarnpkg.com/moment/-/moment-2.22.2.tgz#3c257f9839fc0e93ff53149632239eb90783ff66" @@ -12029,6 +12029,10 @@ tslib@^1.8.0, tslib@^1.8.1, tslib@^1.9.0: version "1.9.2" resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.9.2.tgz#8be0cc9a1f6dc7727c38deb16c2ebd1a2892988e" +tslib@^1.9.3: + version "1.9.3" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.9.3.tgz#d7e4dd79245d85428c4d7e4822a79917954ca286" + tslint-loader@^3.5.3: version "3.6.0" resolved "https://registry.yarnpkg.com/tslint-loader/-/tslint-loader-3.6.0.tgz#12ed4d5ef57d68be25cd12692fb2108b66469d76" From 931b944cddb879dfbfb44c5da18bfda43d36a0e9 Mon Sep 17 00:00:00 2001 From: Tobias Skarhed Date: Wed, 25 Jul 2018 17:38:45 +0200 Subject: [PATCH 168/786] Almost all tests passing --- .../specs/variable_srv_init.jest.ts | 42 +++++-------------- .../app/features/templating/variable_srv.ts | 1 - 2 files changed, 10 insertions(+), 33 deletions(-) diff --git a/public/app/features/templating/specs/variable_srv_init.jest.ts b/public/app/features/templating/specs/variable_srv_init.jest.ts index 519adc0a350..eba0ba8cfee 100644 --- a/public/app/features/templating/specs/variable_srv_init.jest.ts +++ b/public/app/features/templating/specs/variable_srv_init.jest.ts @@ -1,13 +1,9 @@ -//import { describe, beforeEach, it, sinon, expect, angularMocks } from 'test/lib/common'; - import '../all'; import _ from 'lodash'; -// import helpers from 'test/specs/helpers'; -// import { Emitter } from 'app/core/core'; import { VariableSrv } from '../variable_srv'; import $q from 'q'; -// import { model } from 'mobx-state-tree/dist/internal'; +// import { TemplateSrv } from '../template_srv'; describe('VariableSrv init', function() { let templateSrv = { @@ -16,8 +12,9 @@ describe('VariableSrv init', function() { }, variableInitialized: () => {}, updateTemplateData: () => {}, - replace: str => str, + replace: () => ' /pee$/', }; + // let templateSrv = new TemplateSrv(); let $injector = {}; let $rootscope = { $on: () => {}, @@ -29,29 +26,8 @@ describe('VariableSrv init', function() { dashboard: {}, }; - // beforeEach(angularMocks.module('grafana.core')); - // beforeEach(angularMocks.module('grafana.controllers')); - // beforeEach(angularMocks.module('grafana.services')); - // beforeEach( - // angularMocks.module(function($compileProvider) { - // $compileProvider.preAssignBindingsEnabled(true); - // }) - // ); - - // beforeEach(ctx.providePhase(['datasourceSrv', 'timeSrv', 'templateSrv', '$location'])); - // beforeEach( - // angularMocks.inject(($rootScope, $q, $location, $injector) => { - // ctx.$q = $q; - // ctx.$rootScope = $rootScope; - // ctx.$location = $location; - // ctx.variableSrv = $injector.get('variableSrv'); - // ctx.$rootScope.$digest(); - // }) - // ); - function describeInitScenario(desc, fn) { describe(desc, function() { - // events: new Emitter(), var scenario: any = { urlParams: {}, setup: setupFn => { @@ -81,14 +57,12 @@ describe('VariableSrv init', function() { ctx.variableSrv.datasource = ctx.datasource; ctx.variableSrv.datasourceSrv = ctx.datasourceSrv; - ctx.variableSrv.$location.search = () => Promise.resolve(scenario.urlParams); + ctx.variableSrv.$location.search = () => scenario.urlParams; ctx.variableSrv.dashboard = { templating: { list: scenario.variables }, - // events: new Emitter(), }; await ctx.variableSrv.init(ctx.variableSrv.dashboard); - // ctx.$rootScope.$digest(); scenario.variables = ctx.variableSrv.variables; }); @@ -113,6 +87,7 @@ describe('VariableSrv init', function() { }); it('should update current value', () => { + console.log(type); expect(scenario.variables[0].current.value).toBe('new'); expect(scenario.variables[0].current.text).toBe('new'); }); @@ -176,6 +151,7 @@ describe('VariableSrv init', function() { }); it('should update current value', function() { + console.log(ctx.variableSrv.variables[0].options); var variable = ctx.variableSrv.variables[0]; expect(variable.options.length).toBe(2); }); @@ -251,14 +227,16 @@ describe('VariableSrv init', function() { }); function getVarMockConstructor(variable, model, ctx) { - console.log(model.model.type); + // console.log(model.model.type); switch (model.model.type) { case 'datasource': - return new variable(model.model, ctx.datasourceSrv, ctx.templateSrv, ctx.variableSrv); + return new variable(model.model, ctx.datasourceSrv, ctx.variableSrv, ctx.templateSrv); case 'query': return new variable(model.model, ctx.datasourceSrv, ctx.templateSrv, ctx.variableSrv); case 'interval': return new variable(model.model, {}, ctx.templateSrv, ctx.variableSrv); + case 'custom': + return new variable(model.model, ctx.variableSrv); default: return new variable(model.model); } diff --git a/public/app/features/templating/variable_srv.ts b/public/app/features/templating/variable_srv.ts index 9f6522c9b86..8ad3c2845e2 100644 --- a/public/app/features/templating/variable_srv.ts +++ b/public/app/features/templating/variable_srv.ts @@ -23,7 +23,6 @@ export class VariableSrv { // init variables for (let variable of this.variables) { - console.log(variable); variable.initLock = this.$q.defer(); } From 35cc85bfcc46efdc79cf22b98741a6ea34b93d58 Mon Sep 17 00:00:00 2001 From: Tobias Skarhed Date: Thu, 26 Jul 2018 09:36:46 +0200 Subject: [PATCH 169/786] All tests passing. Remove Karma test. --- .../specs/variable_srv_init.jest.ts | 31 ++- .../specs/variable_srv_init_specs.ts | 216 ------------------ 2 files changed, 13 insertions(+), 234 deletions(-) delete mode 100644 public/app/features/templating/specs/variable_srv_init_specs.ts diff --git a/public/app/features/templating/specs/variable_srv_init.jest.ts b/public/app/features/templating/specs/variable_srv_init.jest.ts index eba0ba8cfee..ea8689f528b 100644 --- a/public/app/features/templating/specs/variable_srv_init.jest.ts +++ b/public/app/features/templating/specs/variable_srv_init.jest.ts @@ -3,7 +3,6 @@ import '../all'; import _ from 'lodash'; import { VariableSrv } from '../variable_srv'; import $q from 'q'; -// import { TemplateSrv } from '../template_srv'; describe('VariableSrv init', function() { let templateSrv = { @@ -12,22 +11,21 @@ describe('VariableSrv init', function() { }, variableInitialized: () => {}, updateTemplateData: () => {}, - replace: () => ' /pee$/', + replace: str => + str.replace(this.regex, match => { + return match; + }), }; - // let templateSrv = new TemplateSrv(); + let $injector = {}; let $rootscope = { $on: () => {}, }; - let ctx = { - datasourceSrv: {}, - $location: {}, - dashboard: {}, - }; + let ctx = {}; function describeInitScenario(desc, fn) { - describe(desc, function() { + describe(desc, () => { var scenario: any = { urlParams: {}, setup: setupFn => { @@ -43,7 +41,7 @@ describe('VariableSrv init', function() { }, datasourceSrv: { get: () => Promise.resolve(ctx.datasource), - getMetricSources: () => Promise.resolve(scenario.metricSources), + getMetricSources: () => scenario.metricSources, }, templateSrv, }; @@ -87,7 +85,6 @@ describe('VariableSrv init', function() { }); it('should update current value', () => { - console.log(type); expect(scenario.variables[0].current.value).toBe('new'); expect(scenario.variables[0].current.text).toBe('new'); }); @@ -150,8 +147,7 @@ describe('VariableSrv init', function() { ]; }); - it('should update current value', function() { - console.log(ctx.variableSrv.variables[0].options); + it('should update current value', () => { var variable = ctx.variableSrv.variables[0]; expect(variable.options.length).toBe(2); }); @@ -175,7 +171,7 @@ describe('VariableSrv init', function() { scenario.urlParams['var-apps'] = ['val2', 'val1']; }); - it('should update current value', function() { + it('should update current value', () => { var variable = ctx.variableSrv.variables[0]; expect(variable.current.value.length).toBe(2); expect(variable.current.value[0]).toBe('val2'); @@ -185,7 +181,7 @@ describe('VariableSrv init', function() { expect(variable.options[1].selected).toBe(true); }); - it('should set options that are not in value to selected false', function() { + it('should set options that are not in value to selected false', () => { var variable = ctx.variableSrv.variables[0]; expect(variable.options[2].selected).toBe(false); }); @@ -209,7 +205,7 @@ describe('VariableSrv init', function() { scenario.urlParams['var-apps'] = ['val2', 'val1']; }); - it('should update current value', function() { + it('should update current value', () => { var variable = ctx.variableSrv.variables[0]; expect(variable.current.value.length).toBe(2); expect(variable.current.value[0]).toBe('val2'); @@ -219,7 +215,7 @@ describe('VariableSrv init', function() { expect(variable.options[1].selected).toBe(true); }); - it('should set options that are not in value to selected false', function() { + it('should set options that are not in value to selected false', () => { var variable = ctx.variableSrv.variables[0]; expect(variable.options[2].selected).toBe(false); }); @@ -227,7 +223,6 @@ describe('VariableSrv init', function() { }); function getVarMockConstructor(variable, model, ctx) { - // console.log(model.model.type); switch (model.model.type) { case 'datasource': return new variable(model.model, ctx.datasourceSrv, ctx.variableSrv, ctx.templateSrv); diff --git a/public/app/features/templating/specs/variable_srv_init_specs.ts b/public/app/features/templating/specs/variable_srv_init_specs.ts deleted file mode 100644 index 11639c6aa8f..00000000000 --- a/public/app/features/templating/specs/variable_srv_init_specs.ts +++ /dev/null @@ -1,216 +0,0 @@ -import { describe, beforeEach, it, sinon, expect, angularMocks } from 'test/lib/common'; - -import '../all'; - -import _ from 'lodash'; -import helpers from 'test/specs/helpers'; -import { Emitter } from 'app/core/core'; - -describe('VariableSrv init', function() { - var ctx = new helpers.ControllerTestContext(); - - beforeEach(angularMocks.module('grafana.core')); - beforeEach(angularMocks.module('grafana.controllers')); - beforeEach(angularMocks.module('grafana.services')); - beforeEach( - angularMocks.module(function($compileProvider) { - $compileProvider.preAssignBindingsEnabled(true); - }) - ); - - beforeEach(ctx.providePhase(['datasourceSrv', 'timeSrv', 'templateSrv', '$location'])); - beforeEach( - angularMocks.inject(($rootScope, $q, $location, $injector) => { - ctx.$q = $q; - ctx.$rootScope = $rootScope; - ctx.$location = $location; - ctx.variableSrv = $injector.get('variableSrv'); - ctx.$rootScope.$digest(); - }) - ); - - function describeInitScenario(desc, fn) { - describe(desc, function() { - var scenario: any = { - urlParams: {}, - setup: setupFn => { - scenario.setupFn = setupFn; - }, - }; - - beforeEach(function() { - scenario.setupFn(); - ctx.datasource = {}; - ctx.datasource.metricFindQuery = sinon.stub().returns(ctx.$q.when(scenario.queryResult)); - - ctx.datasourceSrv.get = sinon.stub().returns(ctx.$q.when(ctx.datasource)); - ctx.datasourceSrv.getMetricSources = sinon.stub().returns(scenario.metricSources); - - ctx.$location.search = sinon.stub().returns(scenario.urlParams); - ctx.dashboard = { - templating: { list: scenario.variables }, - events: new Emitter(), - }; - - ctx.variableSrv.init(ctx.dashboard); - ctx.$rootScope.$digest(); - - scenario.variables = ctx.variableSrv.variables; - }); - - fn(scenario); - }); - } - - ['query', 'interval', 'custom', 'datasource'].forEach(type => { - describeInitScenario('when setting ' + type + ' variable via url', scenario => { - scenario.setup(() => { - scenario.variables = [ - { - name: 'apps', - type: type, - current: { text: 'test', value: 'test' }, - options: [{ text: 'test', value: 'test' }], - }, - ]; - scenario.urlParams['var-apps'] = 'new'; - scenario.metricSources = []; - }); - - it('should update current value', () => { - expect(scenario.variables[0].current.value).to.be('new'); - expect(scenario.variables[0].current.text).to.be('new'); - }); - }); - }); - - describe('given dependent variables', () => { - var variableList = [ - { - name: 'app', - type: 'query', - query: '', - current: { text: 'app1', value: 'app1' }, - options: [{ text: 'app1', value: 'app1' }], - }, - { - name: 'server', - type: 'query', - refresh: 1, - query: '$app.*', - current: { text: 'server1', value: 'server1' }, - options: [{ text: 'server1', value: 'server1' }], - }, - ]; - - describeInitScenario('when setting parent var from url', scenario => { - scenario.setup(() => { - scenario.variables = _.cloneDeep(variableList); - scenario.urlParams['var-app'] = 'google'; - scenario.queryResult = [{ text: 'google-server1' }, { text: 'google-server2' }]; - }); - - it('should update child variable', () => { - expect(scenario.variables[1].options.length).to.be(2); - expect(scenario.variables[1].current.text).to.be('google-server1'); - }); - - it('should only update it once', () => { - expect(ctx.datasource.metricFindQuery.callCount).to.be(1); - }); - }); - }); - - describeInitScenario('when datasource variable is initialized', scenario => { - scenario.setup(() => { - scenario.variables = [ - { - type: 'datasource', - query: 'graphite', - name: 'test', - current: { value: 'backend4_pee', text: 'backend4_pee' }, - regex: '/pee$/', - }, - ]; - scenario.metricSources = [ - { name: 'backend1', meta: { id: 'influx' } }, - { name: 'backend2_pee', meta: { id: 'graphite' } }, - { name: 'backend3', meta: { id: 'graphite' } }, - { name: 'backend4_pee', meta: { id: 'graphite' } }, - ]; - }); - - it('should update current value', function() { - var variable = ctx.variableSrv.variables[0]; - expect(variable.options.length).to.be(2); - }); - }); - - describeInitScenario('when template variable is present in url multiple times', scenario => { - scenario.setup(() => { - scenario.variables = [ - { - name: 'apps', - type: 'query', - multi: true, - current: { text: 'val1', value: 'val1' }, - options: [ - { text: 'val1', value: 'val1' }, - { text: 'val2', value: 'val2' }, - { text: 'val3', value: 'val3', selected: true }, - ], - }, - ]; - scenario.urlParams['var-apps'] = ['val2', 'val1']; - }); - - it('should update current value', function() { - var variable = ctx.variableSrv.variables[0]; - expect(variable.current.value.length).to.be(2); - expect(variable.current.value[0]).to.be('val2'); - expect(variable.current.value[1]).to.be('val1'); - expect(variable.current.text).to.be('val2 + val1'); - expect(variable.options[0].selected).to.be(true); - expect(variable.options[1].selected).to.be(true); - }); - - it('should set options that are not in value to selected false', function() { - var variable = ctx.variableSrv.variables[0]; - expect(variable.options[2].selected).to.be(false); - }); - }); - - describeInitScenario('when template variable is present in url multiple times using key/values', scenario => { - scenario.setup(() => { - scenario.variables = [ - { - name: 'apps', - type: 'query', - multi: true, - current: { text: 'Val1', value: 'val1' }, - options: [ - { text: 'Val1', value: 'val1' }, - { text: 'Val2', value: 'val2' }, - { text: 'Val3', value: 'val3', selected: true }, - ], - }, - ]; - scenario.urlParams['var-apps'] = ['val2', 'val1']; - }); - - it('should update current value', function() { - var variable = ctx.variableSrv.variables[0]; - expect(variable.current.value.length).to.be(2); - expect(variable.current.value[0]).to.be('val2'); - expect(variable.current.value[1]).to.be('val1'); - expect(variable.current.text).to.be('Val2 + Val1'); - expect(variable.options[0].selected).to.be(true); - expect(variable.options[1].selected).to.be(true); - }); - - it('should set options that are not in value to selected false', function() { - var variable = ctx.variableSrv.variables[0]; - expect(variable.options[2].selected).to.be(false); - }); - }); -}); From 88e91b3f51fa2c5a66442bfa3322abbfbeebd950 Mon Sep 17 00:00:00 2001 From: Tobias Skarhed Date: Thu, 26 Jul 2018 10:44:40 +0200 Subject: [PATCH 170/786] Begin conversion --- .../panel/singlestat/specs/singlestat.jest.ts | 384 ++++++++++++++++++ 1 file changed, 384 insertions(+) create mode 100644 public/app/plugins/panel/singlestat/specs/singlestat.jest.ts diff --git a/public/app/plugins/panel/singlestat/specs/singlestat.jest.ts b/public/app/plugins/panel/singlestat/specs/singlestat.jest.ts new file mode 100644 index 00000000000..2c945aa6eb2 --- /dev/null +++ b/public/app/plugins/panel/singlestat/specs/singlestat.jest.ts @@ -0,0 +1,384 @@ +// import { describe, beforeEach, afterEach, it, sinon, expect, angularMocks } from 'test/lib/common'; + +// import helpers from 'test/specs/helpers'; +import { SingleStatCtrl } from '../module'; +import moment from 'moment'; + +describe('SingleStatCtrl', function() { + let ctx = {}; + let epoch = 1505826363746; + let clock; + + let $scope = { + $on: () => {}, + }; + + let $injector = { + get: () => {}, + }; + + SingleStatCtrl.prototype.panel = { + events: { + on: () => {}, + emit: () => {}, + }, + }; + SingleStatCtrl.prototype.dashboard = { + isTimezoneUtc: () => {}, + }; + + function singleStatScenario(desc, func) { + describe(desc, function() { + ctx.setup = function(setupFunc) { + // beforeEach(angularMocks.module('grafana.services')); + // beforeEach(angularMocks.module('grafana.controllers')); + // beforeEach( + // angularMocks.module(function($compileProvider) { + // $compileProvider.preAssignBindingsEnabled(true); + // }) + // ); + + // beforeEach(ctx.providePhase()); + // beforeEach(ctx.createPanelController(SingleStatCtrl)); + + beforeEach(function() { + ctx.ctrl = new SingleStatCtrl($scope, $injector, {}); + setupFunc(); + ctx.ctrl.onDataReceived(ctx.data); + ctx.data = ctx.ctrl.data; + }); + }; + + func(ctx); + }); + } + + singleStatScenario('with defaults', function(ctx) { + ctx.setup(function() { + ctx.data = [{ target: 'test.cpu1', datapoints: [[10, 1], [20, 2]] }]; + }); + + it('Should use series avg as default main value', function() { + expect(ctx.data.value).toBe(15); + expect(ctx.data.valueRounded).toBe(15); + }); + + it('should set formatted falue', function() { + expect(ctx.data.valueFormatted).toBe('15'); + }); + }); + + singleStatScenario('showing serie name instead of value', function(ctx) { + ctx.setup(function() { + ctx.data = [{ target: 'test.cpu1', datapoints: [[10, 1], [20, 2]] }]; + ctx.ctrl.panel.valueName = 'name'; + }); + + it('Should use series avg as default main value', function() { + expect(ctx.data.value).toBe(0); + expect(ctx.data.valueRounded).toBe(0); + }); + + it('should set formatted value', function() { + expect(ctx.data.valueFormatted).toBe('test.cpu1'); + }); + }); + + singleStatScenario('showing last iso time instead of value', function(ctx) { + ctx.setup(function() { + ctx.data = [{ target: 'test.cpu1', datapoints: [[10, 12], [20, 1505634997920]] }]; + ctx.ctrl.panel.valueName = 'last_time'; + ctx.ctrl.panel.format = 'dateTimeAsIso'; + }); + + it('Should use time instead of value', function() { + console.log(ctx.data.value); + expect(ctx.data.value).toBe(1505634997920); + expect(ctx.data.valueRounded).toBe(1505634997920); + }); + + it('should set formatted value', function() { + expect(ctx.data.valueFormatted).toBe(moment(1505634997920).format('YYYY-MM-DD HH:mm:ss')); + }); + }); + + singleStatScenario('showing last iso time instead of value (in UTC)', function(ctx) { + ctx.setup(function() { + ctx.data = [{ target: 'test.cpu1', datapoints: [[10, 12], [20, 1505634997920]] }]; + ctx.ctrl.panel.valueName = 'last_time'; + ctx.ctrl.panel.format = 'dateTimeAsIso'; + // ctx.setIsUtc(true); + }); + + it('should set formatted value', function() { + expect(ctx.data.valueFormatted).toBe(moment.utc(1505634997920).format('YYYY-MM-DD HH:mm:ss')); + }); + }); + + singleStatScenario('showing last us time instead of value', function(ctx) { + ctx.setup(function() { + ctx.data = [{ target: 'test.cpu1', datapoints: [[10, 12], [20, 1505634997920]] }]; + ctx.ctrl.panel.valueName = 'last_time'; + ctx.ctrl.panel.format = 'dateTimeAsUS'; + }); + + it('Should use time instead of value', function() { + expect(ctx.data.value).toBe(1505634997920); + expect(ctx.data.valueRounded).toBe(1505634997920); + }); + + it('should set formatted value', function() { + expect(ctx.data.valueFormatted).toBe(moment(1505634997920).format('MM/DD/YYYY h:mm:ss a')); + }); + }); + + singleStatScenario('showing last us time instead of value (in UTC)', function(ctx) { + ctx.setup(function() { + ctx.data = [{ target: 'test.cpu1', datapoints: [[10, 12], [20, 1505634997920]] }]; + ctx.ctrl.panel.valueName = 'last_time'; + ctx.ctrl.panel.format = 'dateTimeAsUS'; + // ctx.setIsUtc(true); + }); + + it('should set formatted value', function() { + expect(ctx.data.valueFormatted).toBe(moment.utc(1505634997920).format('MM/DD/YYYY h:mm:ss a')); + }); + }); + + singleStatScenario('showing last time from now instead of value', function(ctx) { + beforeEach(() => { + // clock = sinon.useFakeTimers(epoch); + jest.useFakeTimers(); + }); + + ctx.setup(function() { + ctx.data = [{ target: 'test.cpu1', datapoints: [[10, 12], [20, 1505634997920]] }]; + ctx.ctrl.panel.valueName = 'last_time'; + ctx.ctrl.panel.format = 'dateTimeFromNow'; + }); + + it('Should use time instead of value', function() { + expect(ctx.data.value).toBe(1505634997920); + expect(ctx.data.valueRounded).toBe(1505634997920); + }); + + it('should set formatted value', function() { + expect(ctx.data.valueFormatted).toBe('2 days ago'); + }); + + afterEach(() => { + jest.clearAllTimers(); + }); + }); + + singleStatScenario('showing last time from now instead of value (in UTC)', function(ctx) { + beforeEach(() => { + // clock = sinon.useFakeTimers(epoch); + jest.useFakeTimers(); + }); + + ctx.setup(function() { + ctx.data = [{ target: 'test.cpu1', datapoints: [[10, 12], [20, 1505634997920]] }]; + ctx.ctrl.panel.valueName = 'last_time'; + ctx.ctrl.panel.format = 'dateTimeFromNow'; + // ctx.setIsUtc(true); + }); + + it('should set formatted value', function() { + expect(ctx.data.valueFormatted).toBe('2 days ago'); + }); + + afterEach(() => { + jest.clearAllTimers(); + }); + }); + + singleStatScenario('MainValue should use same number for decimals as displayed when checking thresholds', function( + ctx + ) { + ctx.setup(function() { + ctx.data = [{ target: 'test.cpu1', datapoints: [[99.999, 1], [99.99999, 2]] }]; + }); + + it('Should be rounded', function() { + expect(ctx.data.value).toBe(99.999495); + expect(ctx.data.valueRounded).toBe(100); + }); + + it('should set formatted value', function() { + expect(ctx.data.valueFormatted).toBe('100'); + }); + }); + + singleStatScenario('When value to text mapping is specified', function(ctx) { + ctx.setup(function() { + ctx.data = [{ target: 'test.cpu1', datapoints: [[9.9, 1]] }]; + ctx.ctrl.panel.valueMaps = [{ value: '10', text: 'OK' }]; + }); + + it('value should remain', function() { + expect(ctx.data.value).toBe(9.9); + }); + + it('round should be rounded up', function() { + expect(ctx.data.valueRounded).toBe(10); + }); + + it('Should replace value with text', function() { + expect(ctx.data.valueFormatted).toBe('OK'); + }); + }); + + singleStatScenario('When range to text mapping is specified for first range', function(ctx) { + ctx.setup(function() { + ctx.data = [{ target: 'test.cpu1', datapoints: [[41, 50]] }]; + ctx.ctrl.panel.mappingType = 2; + ctx.ctrl.panel.rangeMaps = [{ from: '10', to: '50', text: 'OK' }, { from: '51', to: '100', text: 'NOT OK' }]; + }); + + it('Should replace value with text OK', function() { + expect(ctx.data.valueFormatted).toBe('OK'); + }); + }); + + singleStatScenario('When range to text mapping is specified for other ranges', function(ctx) { + ctx.setup(function() { + ctx.data = [{ target: 'test.cpu1', datapoints: [[65, 75]] }]; + ctx.ctrl.panel.mappingType = 2; + ctx.ctrl.panel.rangeMaps = [{ from: '10', to: '50', text: 'OK' }, { from: '51', to: '100', text: 'NOT OK' }]; + }); + + it('Should replace value with text NOT OK', function() { + expect(ctx.data.valueFormatted).toBe('NOT OK'); + }); + }); + + describe('When table data', function() { + const tableData = [ + { + columns: [{ text: 'Time', type: 'time' }, { text: 'test1' }, { text: 'mean' }, { text: 'test2' }], + rows: [[1492759673649, 'ignore1', 15, 'ignore2']], + type: 'table', + }, + ]; + + singleStatScenario('with default values', function(ctx) { + ctx.setup(function() { + ctx.data = tableData; + ctx.ctrl.panel.tableColumn = 'mean'; + }); + + it('Should use first rows value as default main value', function() { + expect(ctx.data.value).toBe(15); + expect(ctx.data.valueRounded).toBe(15); + }); + + it('should set formatted value', function() { + expect(ctx.data.valueFormatted).toBe('15'); + }); + }); + + singleStatScenario('When table data has multiple columns', function(ctx) { + ctx.setup(function() { + ctx.data = tableData; + ctx.ctrl.panel.tableColumn = ''; + }); + + it('Should set column to first column that is not time', function() { + expect(ctx.ctrl.panel.tableColumn).toBe('test1'); + }); + }); + + singleStatScenario('MainValue should use same number for decimals as displayed when checking thresholds', function( + ctx + ) { + ctx.setup(function() { + ctx.data = tableData; + ctx.data[0].rows[0] = [1492759673649, 'ignore1', 99.99999, 'ignore2']; + ctx.ctrl.panel.tableColumn = 'mean'; + }); + + it('Should be rounded', function() { + expect(ctx.data.value).toBe(99.99999); + expect(ctx.data.valueRounded).toBe(100); + }); + + it('should set formatted falue', function() { + expect(ctx.data.valueFormatted).toBe('100'); + }); + }); + + singleStatScenario('When value to text mapping is specified', function(ctx) { + ctx.setup(function() { + ctx.data = tableData; + ctx.data[0].rows[0] = [1492759673649, 'ignore1', 9.9, 'ignore2']; + ctx.ctrl.panel.tableColumn = 'mean'; + ctx.ctrl.panel.valueMaps = [{ value: '10', text: 'OK' }]; + }); + + it('value should remain', function() { + expect(ctx.data.value).toBe(9.9); + }); + + it('round should be rounded up', function() { + expect(ctx.data.valueRounded).toBe(10); + }); + + it('Should replace value with text', function() { + expect(ctx.data.valueFormatted).toBe('OK'); + }); + }); + + singleStatScenario('When range to text mapping is specified for first range', function(ctx) { + ctx.setup(function() { + ctx.data = tableData; + ctx.data[0].rows[0] = [1492759673649, 'ignore1', 41, 'ignore2']; + ctx.ctrl.panel.tableColumn = 'mean'; + ctx.ctrl.panel.mappingType = 2; + ctx.ctrl.panel.rangeMaps = [{ from: '10', to: '50', text: 'OK' }, { from: '51', to: '100', text: 'NOT OK' }]; + }); + + it('Should replace value with text OK', function() { + expect(ctx.data.valueFormatted).toBe('OK'); + }); + }); + + singleStatScenario('When range to text mapping is specified for other ranges', function(ctx) { + ctx.setup(function() { + ctx.data = tableData; + ctx.data[0].rows[0] = [1492759673649, 'ignore1', 65, 'ignore2']; + ctx.ctrl.panel.tableColumn = 'mean'; + ctx.ctrl.panel.mappingType = 2; + ctx.ctrl.panel.rangeMaps = [{ from: '10', to: '50', text: 'OK' }, { from: '51', to: '100', text: 'NOT OK' }]; + }); + + it('Should replace value with text NOT OK', function() { + expect(ctx.data.valueFormatted).toBe('NOT OK'); + }); + }); + + singleStatScenario('When value is string', function(ctx) { + ctx.setup(function() { + ctx.data = tableData; + ctx.data[0].rows[0] = [1492759673649, 'ignore1', 65, 'ignore2']; + ctx.ctrl.panel.tableColumn = 'test1'; + }); + + it('Should replace value with text NOT OK', function() { + expect(ctx.data.valueFormatted).toBe('ignore1'); + }); + }); + + singleStatScenario('When value is zero', function(ctx) { + ctx.setup(function() { + ctx.data = tableData; + ctx.data[0].rows[0] = [1492759673649, 'ignore1', 0, 'ignore2']; + ctx.ctrl.panel.tableColumn = 'mean'; + }); + + it('Should return zero', function() { + expect(ctx.data.value).toBe(0); + }); + }); + }); +}); From 7699451d9438546e6655975d53deb7bf6314562d Mon Sep 17 00:00:00 2001 From: David Date: Thu, 26 Jul 2018 14:04:12 +0200 Subject: [PATCH 171/786] Refactor Explore query field (#12643) * Refactor Explore query field - extract typeahead field that only contains logic for the typeahead mechanics - renamed QueryField to PromQueryField, a wrapper around TypeaheadField that deals with Prometheus-specific concepts - PromQueryField creates a promql typeahead by providing the handlers for producing suggestions, and for applying suggestions - The `refresher` promise is needed to trigger a render once an async action in the wrapper returns. This is prep work for a composable query field to be used by Explore, as well as editors in datasource plugins. * Added typeahead handling tests - extracted context-to-suggestion logic to make it testable - kept DOM-dependent parts in main onTypeahead funtion * simplified error handling in explore query field * Refactor query suggestions - use monaco's suggestion types (roughly), see https://github.com/Microsoft/monaco-editor/blob/f6fb545/monaco.d.ts#L4208 - suggest functions and metrics in empty field (ctrl+space) - copy and expand prometheus function docs from prometheus datasource (will be migrated back to the datasource in the future) * Added prop and state types, removed unused cwrp * Split up suggestion processing for code readability --- .../Explore/PromQueryField.jest.tsx | 125 ++++ .../app/containers/Explore/PromQueryField.tsx | 340 +++++++++++ public/app/containers/Explore/QueryField.tsx | 553 ++++++++---------- public/app/containers/Explore/QueryRows.tsx | 6 +- public/app/containers/Explore/Typeahead.tsx | 61 +- .../Explore/slate-plugins/prism/promql.ts | 417 +++++++++++-- public/sass/components/_slate_editor.scss | 1 + 7 files changed, 1100 insertions(+), 403 deletions(-) create mode 100644 public/app/containers/Explore/PromQueryField.jest.tsx create mode 100644 public/app/containers/Explore/PromQueryField.tsx diff --git a/public/app/containers/Explore/PromQueryField.jest.tsx b/public/app/containers/Explore/PromQueryField.jest.tsx new file mode 100644 index 00000000000..8d2903cb2c2 --- /dev/null +++ b/public/app/containers/Explore/PromQueryField.jest.tsx @@ -0,0 +1,125 @@ +import React from 'react'; +import Enzyme, { shallow } from 'enzyme'; +import Adapter from 'enzyme-adapter-react-16'; + +Enzyme.configure({ adapter: new Adapter() }); + +import PromQueryField from './PromQueryField'; + +describe('PromQueryField typeahead handling', () => { + const defaultProps = { + request: () => ({ data: { data: [] } }), + }; + + it('returns default suggestions on emtpty context', () => { + const instance = shallow().instance() as PromQueryField; + const result = instance.getTypeahead({ text: '', prefix: '', wrapperClasses: [] }); + expect(result.context).toBeUndefined(); + expect(result.refresher).toBeUndefined(); + expect(result.suggestions.length).toEqual(2); + }); + + describe('range suggestions', () => { + it('returns range suggestions in range context', () => { + const instance = shallow().instance() as PromQueryField; + const result = instance.getTypeahead({ text: '1', prefix: '1', wrapperClasses: ['context-range'] }); + expect(result.context).toBe('context-range'); + expect(result.refresher).toBeUndefined(); + expect(result.suggestions).toEqual([ + { + items: [{ label: '1m' }, { label: '5m' }, { label: '10m' }, { label: '30m' }, { label: '1h' }], + label: 'Range vector', + }, + ]); + }); + }); + + describe('metric suggestions', () => { + it('returns metrics suggestions by default', () => { + const instance = shallow( + + ).instance() as PromQueryField; + const result = instance.getTypeahead({ text: 'a', prefix: 'a', wrapperClasses: [] }); + expect(result.context).toBeUndefined(); + expect(result.refresher).toBeUndefined(); + expect(result.suggestions.length).toEqual(2); + }); + + it('returns default suggestions after a binary operator', () => { + const instance = shallow( + + ).instance() as PromQueryField; + const result = instance.getTypeahead({ text: '*', prefix: '', wrapperClasses: [] }); + expect(result.context).toBeUndefined(); + expect(result.refresher).toBeUndefined(); + expect(result.suggestions.length).toEqual(2); + }); + }); + + describe('label suggestions', () => { + it('returns default label suggestions on label context and no metric', () => { + const instance = shallow().instance() as PromQueryField; + const result = instance.getTypeahead({ text: 'j', prefix: 'j', wrapperClasses: ['context-labels'] }); + expect(result.context).toBe('context-labels'); + expect(result.suggestions).toEqual([{ items: [{ label: 'job' }, { label: 'instance' }], label: 'Labels' }]); + }); + + it('returns label suggestions on label context and metric', () => { + const instance = shallow( + + ).instance() as PromQueryField; + const result = instance.getTypeahead({ + text: 'job', + prefix: 'job', + wrapperClasses: ['context-labels'], + metric: 'foo', + }); + expect(result.context).toBe('context-labels'); + expect(result.suggestions).toEqual([{ items: [{ label: 'bar' }], label: 'Labels' }]); + }); + + it('returns a refresher on label context and unavailable metric', () => { + const instance = shallow( + + ).instance() as PromQueryField; + const result = instance.getTypeahead({ + text: 'job', + prefix: 'job', + wrapperClasses: ['context-labels'], + metric: 'xxx', + }); + expect(result.context).toBeUndefined(); + expect(result.refresher).toBeInstanceOf(Promise); + expect(result.suggestions).toEqual([]); + }); + + it('returns label values on label context when given a metric and a label key', () => { + const instance = shallow( + + ).instance() as PromQueryField; + const result = instance.getTypeahead({ + text: '=ba', + prefix: 'ba', + wrapperClasses: ['context-labels'], + metric: 'foo', + labelKey: 'bar', + }); + expect(result.context).toBe('context-label-values'); + expect(result.suggestions).toEqual([{ items: [{ label: 'baz' }], label: 'Label values' }]); + }); + + it('returns label suggestions on aggregation context and metric', () => { + const instance = shallow( + + ).instance() as PromQueryField; + const result = instance.getTypeahead({ + text: 'job', + prefix: 'job', + wrapperClasses: ['context-aggregation'], + metric: 'foo', + }); + expect(result.context).toBe('context-aggregation'); + expect(result.suggestions).toEqual([{ items: [{ label: 'bar' }], label: 'Labels' }]); + }); + }); +}); diff --git a/public/app/containers/Explore/PromQueryField.tsx b/public/app/containers/Explore/PromQueryField.tsx new file mode 100644 index 00000000000..eb8fc25c67f --- /dev/null +++ b/public/app/containers/Explore/PromQueryField.tsx @@ -0,0 +1,340 @@ +import _ from 'lodash'; +import React from 'react'; + +// dom also includes Element polyfills +import { getNextCharacter, getPreviousCousin } from './utils/dom'; +import PluginPrism, { setPrismTokens } from './slate-plugins/prism/index'; +import PrismPromql, { FUNCTIONS } from './slate-plugins/prism/promql'; +import RunnerPlugin from './slate-plugins/runner'; +import { processLabels, RATE_RANGES, cleanText } from './utils/prometheus'; + +import TypeaheadField, { + Suggestion, + SuggestionGroup, + TypeaheadInput, + TypeaheadFieldState, + TypeaheadOutput, +} from './QueryField'; + +const EMPTY_METRIC = ''; +const METRIC_MARK = 'metric'; +const PRISM_LANGUAGE = 'promql'; + +export const wrapLabel = label => ({ label }); +export const setFunctionMove = (suggestion: Suggestion): Suggestion => { + suggestion.move = -1; + return suggestion; +}; + +export function willApplySuggestion( + suggestion: string, + { typeaheadContext, typeaheadText }: TypeaheadFieldState +): string { + // Modify suggestion based on context + switch (typeaheadContext) { + case 'context-labels': { + const nextChar = getNextCharacter(); + if (!nextChar || nextChar === '}' || nextChar === ',') { + suggestion += '='; + } + break; + } + + case 'context-label-values': { + // Always add quotes and remove existing ones instead + if (!(typeaheadText.startsWith('="') || typeaheadText.startsWith('"'))) { + suggestion = `"${suggestion}`; + } + if (getNextCharacter() !== '"') { + suggestion = `${suggestion}"`; + } + break; + } + + default: + } + return suggestion; +} + +interface PromQueryFieldProps { + initialQuery?: string | null; + labelKeys?: { [index: string]: string[] }; // metric -> [labelKey,...] + labelValues?: { [index: string]: { [index: string]: string[] } }; // metric -> labelKey -> [labelValue,...] + metrics?: string[]; + onPressEnter?: () => void; + onQueryChange?: (value: string) => void; + portalPrefix?: string; + request?: (url: string) => any; +} + +interface PromQueryFieldState { + labelKeys: { [index: string]: string[] }; // metric -> [labelKey,...] + labelValues: { [index: string]: { [index: string]: string[] } }; // metric -> labelKey -> [labelValue,...] + metrics: string[]; +} + +interface PromTypeaheadInput { + text: string; + prefix: string; + wrapperClasses: string[]; + metric?: string; + labelKey?: string; +} + +class PromQueryField extends React.Component { + plugins: any[]; + + constructor(props, context) { + super(props, context); + + this.plugins = [ + RunnerPlugin({ handler: props.onPressEnter }), + PluginPrism({ definition: PrismPromql, language: PRISM_LANGUAGE }), + ]; + + this.state = { + labelKeys: props.labelKeys || {}, + labelValues: props.labelValues || {}, + metrics: props.metrics || [], + }; + } + + componentDidMount() { + this.fetchMetricNames(); + } + + onChangeQuery = value => { + // Send text change to parent + const { onQueryChange } = this.props; + if (onQueryChange) { + onQueryChange(value); + } + }; + + onReceiveMetrics = () => { + if (!this.state.metrics) { + return; + } + setPrismTokens(PRISM_LANGUAGE, METRIC_MARK, this.state.metrics); + }; + + onTypeahead = (typeahead: TypeaheadInput): TypeaheadOutput => { + const { editorNode, prefix, text, wrapperNode } = typeahead; + + // Get DOM-dependent context + const wrapperClasses = Array.from(wrapperNode.classList); + // Take first metric as lucky guess + const metricNode = editorNode.querySelector(`.${METRIC_MARK}`); + const metric = metricNode && metricNode.textContent; + const labelKeyNode = getPreviousCousin(wrapperNode, '.attr-name'); + const labelKey = labelKeyNode && labelKeyNode.textContent; + + const result = this.getTypeahead({ text, prefix, wrapperClasses, metric, labelKey }); + + console.log('handleTypeahead', wrapperClasses, text, prefix, result.context); + + return result; + }; + + // Keep this DOM-free for testing + getTypeahead({ prefix, wrapperClasses, metric, text }: PromTypeaheadInput): TypeaheadOutput { + // Determine candidates by CSS context + if (_.includes(wrapperClasses, 'context-range')) { + // Suggestions for metric[|] + return this.getRangeTypeahead(); + } else if (_.includes(wrapperClasses, 'context-labels')) { + // Suggestions for metric{|} and metric{foo=|}, as well as metric-independent label queries like {|} + return this.getLabelTypeahead.apply(this, arguments); + } else if (metric && _.includes(wrapperClasses, 'context-aggregation')) { + return this.getAggregationTypeahead.apply(this, arguments); + } else if ( + // Non-empty but not inside known token unless it's a metric + (prefix && !_.includes(wrapperClasses, 'token')) || + prefix === metric || + (prefix === '' && !text.match(/^[)\s]+$/)) || // Empty context or after ')' + text.match(/[+\-*/^%]/) // After binary operator + ) { + return this.getEmptyTypeahead(); + } + + return { + suggestions: [], + }; + } + + getEmptyTypeahead(): TypeaheadOutput { + const suggestions: SuggestionGroup[] = []; + suggestions.push({ + prefixMatch: true, + label: 'Functions', + items: FUNCTIONS.map(setFunctionMove), + }); + + if (this.state.metrics) { + suggestions.push({ + label: 'Metrics', + items: this.state.metrics.map(wrapLabel), + }); + } + return { suggestions }; + } + + getRangeTypeahead(): TypeaheadOutput { + return { + context: 'context-range', + suggestions: [ + { + label: 'Range vector', + items: [...RATE_RANGES].map(wrapLabel), + }, + ], + }; + } + + getAggregationTypeahead({ metric }: PromTypeaheadInput): TypeaheadOutput { + let refresher: Promise = null; + const suggestions: SuggestionGroup[] = []; + const labelKeys = this.state.labelKeys[metric]; + if (labelKeys) { + suggestions.push({ label: 'Labels', items: labelKeys.map(wrapLabel) }); + } else { + refresher = this.fetchMetricLabels(metric); + } + + return { + refresher, + suggestions, + context: 'context-aggregation', + }; + } + + getLabelTypeahead({ metric, text, wrapperClasses, labelKey }: PromTypeaheadInput): TypeaheadOutput { + let context: string; + let refresher: Promise = null; + const suggestions: SuggestionGroup[] = []; + if (metric) { + const labelKeys = this.state.labelKeys[metric]; + if (labelKeys) { + if ((text && text.startsWith('=')) || _.includes(wrapperClasses, 'attr-value')) { + // Label values + if (labelKey) { + const labelValues = this.state.labelValues[metric][labelKey]; + context = 'context-label-values'; + suggestions.push({ + label: 'Label values', + items: labelValues.map(wrapLabel), + }); + } + } else { + // Label keys + context = 'context-labels'; + suggestions.push({ label: 'Labels', items: labelKeys.map(wrapLabel) }); + } + } else { + refresher = this.fetchMetricLabels(metric); + } + } else { + // Metric-independent label queries + const defaultKeys = ['job', 'instance']; + // Munge all keys that we have seen together + const labelKeys = Object.keys(this.state.labelKeys).reduce((acc, metric) => { + return acc.concat(this.state.labelKeys[metric].filter(key => acc.indexOf(key) === -1)); + }, defaultKeys); + if ((text && text.startsWith('=')) || _.includes(wrapperClasses, 'attr-value')) { + // Label values + if (labelKey) { + if (this.state.labelValues[EMPTY_METRIC]) { + const labelValues = this.state.labelValues[EMPTY_METRIC][labelKey]; + context = 'context-label-values'; + suggestions.push({ + label: 'Label values', + items: labelValues.map(wrapLabel), + }); + } else { + // Can only query label values for now (API to query keys is under development) + refresher = this.fetchLabelValues(labelKey); + } + } + } else { + // Label keys + context = 'context-labels'; + suggestions.push({ label: 'Labels', items: labelKeys.map(wrapLabel) }); + } + } + return { context, refresher, suggestions }; + } + + request = url => { + if (this.props.request) { + return this.props.request(url); + } + return fetch(url); + }; + + async fetchLabelValues(key) { + const url = `/api/v1/label/${key}/values`; + try { + const res = await this.request(url); + const body = await (res.data || res.json()); + const pairs = this.state.labelValues[EMPTY_METRIC]; + const values = { + ...pairs, + [key]: body.data, + }; + const labelValues = { + ...this.state.labelValues, + [EMPTY_METRIC]: values, + }; + this.setState({ labelValues }); + } catch (e) { + console.error(e); + } + } + + async fetchMetricLabels(name) { + const url = `/api/v1/series?match[]=${name}`; + try { + const res = await this.request(url); + const body = await (res.data || res.json()); + const { keys, values } = processLabels(body.data); + const labelKeys = { + ...this.state.labelKeys, + [name]: keys, + }; + const labelValues = { + ...this.state.labelValues, + [name]: values, + }; + this.setState({ labelKeys, labelValues }); + } catch (e) { + console.error(e); + } + } + + async fetchMetricNames() { + const url = '/api/v1/label/__name__/values'; + try { + const res = await this.request(url); + const body = await (res.data || res.json()); + this.setState({ metrics: body.data }, this.onReceiveMetrics); + } catch (error) { + console.error(error); + } + } + + render() { + return ( + + ); + } +} + +export default PromQueryField; diff --git a/public/app/containers/Explore/QueryField.tsx b/public/app/containers/Explore/QueryField.tsx index 41f6d53541c..60caddcad31 100644 --- a/public/app/containers/Explore/QueryField.tsx +++ b/public/app/containers/Explore/QueryField.tsx @@ -1,106 +1,163 @@ +import _ from 'lodash'; import React from 'react'; import ReactDOM from 'react-dom'; -import { Value } from 'slate'; +import { Block, Change, Document, Text, Value } from 'slate'; import { Editor } from 'slate-react'; import Plain from 'slate-plain-serializer'; -// dom also includes Element polyfills -import { getNextCharacter, getPreviousCousin } from './utils/dom'; import BracesPlugin from './slate-plugins/braces'; import ClearPlugin from './slate-plugins/clear'; import NewlinePlugin from './slate-plugins/newline'; -import PluginPrism, { setPrismTokens } from './slate-plugins/prism/index'; -import RunnerPlugin from './slate-plugins/runner'; -import debounce from './utils/debounce'; -import { processLabels, RATE_RANGES, cleanText } from './utils/prometheus'; import Typeahead from './Typeahead'; -const EMPTY_METRIC = ''; -const METRIC_MARK = 'metric'; export const TYPEAHEAD_DEBOUNCE = 300; -function flattenSuggestions(s) { +function flattenSuggestions(s: any[]): any[] { return s ? s.reduce((acc, g) => acc.concat(g.items), []) : []; } -export const getInitialValue = query => - Value.fromJSON({ - document: { - nodes: [ - { - object: 'block', - type: 'paragraph', - nodes: [ - { - object: 'text', - leaves: [ - { - text: query, - }, - ], - }, - ], - }, - ], - }, +export const makeFragment = (text: string): Document => { + const lines = text.split('\n').map(line => + Block.create({ + type: 'paragraph', + nodes: [Text.create(line)], + }) + ); + + const fragment = Document.create({ + nodes: lines, }); + return fragment; +}; -class Portal extends React.Component { - node: any; +export const getInitialValue = (value: string): Value => Value.create({ document: makeFragment(value) }); - constructor(props) { - super(props); - const { index = 0, prefix = 'query' } = props; - this.node = document.createElement('div'); - this.node.classList.add(`slate-typeahead`, `slate-typeahead-${prefix}-${index}`); - document.body.appendChild(this.node); - } - - componentWillUnmount() { - document.body.removeChild(this.node); - } - - render() { - return ReactDOM.createPortal(this.props.children, this.node); - } +export interface Suggestion { + /** + * The label of this completion item. By default + * this is also the text that is inserted when selecting + * this completion. + */ + label: string; + /** + * The kind of this completion item. Based on the kind + * an icon is chosen by the editor. + */ + kind?: string; + /** + * A human-readable string with additional information + * about this item, like type or symbol information. + */ + detail?: string; + /** + * A human-readable string, can be Markdown, that represents a doc-comment. + */ + documentation?: string; + /** + * A string that should be used when comparing this item + * with other items. When `falsy` the `label` is used. + */ + sortText?: string; + /** + * A string that should be used when filtering a set of + * completion items. When `falsy` the `label` is used. + */ + filterText?: string; + /** + * A string or snippet that should be inserted in a document when selecting + * this completion. When `falsy` the `label` is used. + */ + insertText?: string; + /** + * Delete number of characters before the caret position, + * by default the letters from the beginning of the word. + */ + deleteBackwards?: number; + /** + * Number of steps to move after the insertion, can be negative. + */ + move?: number; } -class QueryField extends React.Component { - menuEl: any; - plugins: any; +export interface SuggestionGroup { + /** + * Label that will be displayed for all entries of this group. + */ + label: string; + /** + * List of suggestions of this group. + */ + items: Suggestion[]; + /** + * If true, match only by prefix (and not mid-word). + */ + prefixMatch?: boolean; + /** + * If true, do not filter items in this group based on the search. + */ + skipFilter?: boolean; +} + +interface TypeaheadFieldProps { + additionalPlugins?: any[]; + cleanText?: (text: string) => string; + initialValue: string | null; + onBlur?: () => void; + onFocus?: () => void; + onTypeahead?: (typeahead: TypeaheadInput) => TypeaheadOutput; + onValueChanged?: (value: Value) => void; + onWillApplySuggestion?: (suggestion: string, state: TypeaheadFieldState) => string; + placeholder?: string; + portalPrefix?: string; +} + +export interface TypeaheadFieldState { + suggestions: SuggestionGroup[]; + typeaheadContext: string | null; + typeaheadIndex: number; + typeaheadPrefix: string; + typeaheadText: string; + value: Value; +} + +export interface TypeaheadInput { + editorNode: Element; + prefix: string; + selection?: Selection; + text: string; + wrapperNode: Element; +} + +export interface TypeaheadOutput { + context?: string; + refresher?: Promise<{}>; + suggestions: SuggestionGroup[]; +} + +class QueryField extends React.Component { + menuEl: HTMLElement | null; + plugins: any[]; resetTimer: any; constructor(props, context) { super(props, context); - const { prismDefinition = {}, prismLanguage = 'promql' } = props; - - this.plugins = [ - BracesPlugin(), - ClearPlugin(), - RunnerPlugin({ handler: props.onPressEnter }), - NewlinePlugin(), - PluginPrism({ definition: prismDefinition, language: prismLanguage }), - ]; + // Base plugins + this.plugins = [BracesPlugin(), ClearPlugin(), NewlinePlugin(), ...props.additionalPlugins]; this.state = { - labelKeys: {}, - labelValues: {}, - metrics: props.metrics || [], suggestions: [], + typeaheadContext: null, typeaheadIndex: 0, typeaheadPrefix: '', - value: getInitialValue(props.initialQuery || ''), + typeaheadText: '', + value: getInitialValue(props.initialValue || ''), }; } componentDidMount() { this.updateMenu(); - - if (this.props.metrics === undefined) { - this.fetchMetricNames(); - } } componentWillUnmount() { @@ -112,12 +169,9 @@ class QueryField extends React.Component { } componentWillReceiveProps(nextProps) { - if (nextProps.metrics && nextProps.metrics !== this.props.metrics) { - this.setState({ metrics: nextProps.metrics }, this.onMetricsReceived); - } - // initialQuery is null in case the user typed - if (nextProps.initialQuery !== null && nextProps.initialQuery !== this.props.initialQuery) { - this.setState({ value: getInitialValue(nextProps.initialQuery) }); + // initialValue is null in case the user typed + if (nextProps.initialValue !== null && nextProps.initialValue !== this.props.initialValue) { + this.setState({ value: getInitialValue(nextProps.initialValue) }); } } @@ -125,48 +179,28 @@ class QueryField extends React.Component { const changed = value.document !== this.state.value.document; this.setState({ value }, () => { if (changed) { - this.handleChangeQuery(); + this.handleChangeValue(); } }); - window.requestAnimationFrame(this.handleTypeahead); - }; - - onMetricsReceived = () => { - if (!this.state.metrics) { - return; + if (changed) { + window.requestAnimationFrame(this.handleTypeahead); } - setPrismTokens(this.props.prismLanguage, METRIC_MARK, this.state.metrics); - - // Trigger re-render - window.requestAnimationFrame(() => { - // Bogus edit to trigger highlighting - const change = this.state.value - .change() - .insertText(' ') - .deleteBackward(1); - this.onChange(change); - }); }; - request = url => { - if (this.props.request) { - return this.props.request(url); - } - return fetch(url); - }; - - handleChangeQuery = () => { + handleChangeValue = () => { // Send text change to parent - const { onQueryChange } = this.props; - if (onQueryChange) { - onQueryChange(Plain.serialize(this.state.value)); + const { onValueChanged } = this.props; + if (onValueChanged) { + onValueChanged(Plain.serialize(this.state.value)); } }; - handleTypeahead = debounce(() => { + handleTypeahead = _.debounce(async () => { const selection = window.getSelection(); - if (selection.anchorNode) { + const { cleanText, onTypeahead } = this.props; + + if (onTypeahead && selection.anchorNode) { const wrapperNode = selection.anchorNode.parentElement; const editorNode = wrapperNode.closest('.slate-query-field'); if (!editorNode || this.state.value.isBlurred) { @@ -175,164 +209,96 @@ class QueryField extends React.Component { } const range = selection.getRangeAt(0); - const text = selection.anchorNode.textContent; const offset = range.startOffset; - const prefix = cleanText(text.substr(0, offset)); - - // Determine candidates by context - const suggestionGroups = []; - const wrapperClasses = wrapperNode.classList; - let typeaheadContext = null; - - // Take first metric as lucky guess - const metricNode = editorNode.querySelector(`.${METRIC_MARK}`); - - if (wrapperClasses.contains('context-range')) { - // Rate ranges - typeaheadContext = 'context-range'; - suggestionGroups.push({ - label: 'Range vector', - items: [...RATE_RANGES], - }); - } else if (wrapperClasses.contains('context-labels') && metricNode) { - const metric = metricNode.textContent; - const labelKeys = this.state.labelKeys[metric]; - if (labelKeys) { - if ((text && text.startsWith('=')) || wrapperClasses.contains('attr-value')) { - // Label values - const labelKeyNode = getPreviousCousin(wrapperNode, '.attr-name'); - if (labelKeyNode) { - const labelKey = labelKeyNode.textContent; - const labelValues = this.state.labelValues[metric][labelKey]; - typeaheadContext = 'context-label-values'; - suggestionGroups.push({ - label: 'Label values', - items: labelValues, - }); - } - } else { - // Label keys - typeaheadContext = 'context-labels'; - suggestionGroups.push({ label: 'Labels', items: labelKeys }); - } - } else { - this.fetchMetricLabels(metric); - } - } else if (wrapperClasses.contains('context-labels') && !metricNode) { - // Empty name queries - const defaultKeys = ['job', 'instance']; - // Munge all keys that we have seen together - const labelKeys = Object.keys(this.state.labelKeys).reduce((acc, metric) => { - return acc.concat(this.state.labelKeys[metric].filter(key => acc.indexOf(key) === -1)); - }, defaultKeys); - if ((text && text.startsWith('=')) || wrapperClasses.contains('attr-value')) { - // Label values - const labelKeyNode = getPreviousCousin(wrapperNode, '.attr-name'); - if (labelKeyNode) { - const labelKey = labelKeyNode.textContent; - if (this.state.labelValues[EMPTY_METRIC]) { - const labelValues = this.state.labelValues[EMPTY_METRIC][labelKey]; - typeaheadContext = 'context-label-values'; - suggestionGroups.push({ - label: 'Label values', - items: labelValues, - }); - } else { - // Can only query label values for now (API to query keys is under development) - this.fetchLabelValues(labelKey); - } - } - } else { - // Label keys - typeaheadContext = 'context-labels'; - suggestionGroups.push({ label: 'Labels', items: labelKeys }); - } - } else if (metricNode && wrapperClasses.contains('context-aggregation')) { - typeaheadContext = 'context-aggregation'; - const metric = metricNode.textContent; - const labelKeys = this.state.labelKeys[metric]; - if (labelKeys) { - suggestionGroups.push({ label: 'Labels', items: labelKeys }); - } else { - this.fetchMetricLabels(metric); - } - } else if ( - (this.state.metrics && ((prefix && !wrapperClasses.contains('token')) || text.match(/[+\-*/^%]/))) || - wrapperClasses.contains('context-function') - ) { - // Need prefix for metrics - typeaheadContext = 'context-metrics'; - suggestionGroups.push({ - label: 'Metrics', - items: this.state.metrics, - }); + const text = selection.anchorNode.textContent; + let prefix = text.substr(0, offset); + if (cleanText) { + prefix = cleanText(prefix); } - let results = 0; - const filteredSuggestions = suggestionGroups.map(group => { - if (group.items) { - group.items = group.items.filter(c => c.length !== prefix.length && c.indexOf(prefix) > -1); - results += group.items.length; + const { suggestions, context, refresher } = onTypeahead({ + editorNode, + prefix, + selection, + text, + wrapperNode, + }); + + const filteredSuggestions = suggestions + .map(group => { + if (group.items) { + if (prefix) { + // Filter groups based on prefix + if (!group.skipFilter) { + group.items = group.items.filter(c => (c.filterText || c.label).length >= prefix.length); + if (group.prefixMatch) { + group.items = group.items.filter(c => (c.filterText || c.label).indexOf(prefix) === 0); + } else { + group.items = group.items.filter(c => (c.filterText || c.label).indexOf(prefix) > -1); + } + } + // Filter out the already typed value (prefix) unless it inserts custom text + group.items = group.items.filter(c => c.insertText || (c.filterText || c.label) !== prefix); + } + + group.items = _.sortBy(group.items, item => item.sortText || item.label); + } + return group; + }) + .filter(group => group.items && group.items.length > 0); // Filter out empty groups + + this.setState( + { + suggestions: filteredSuggestions, + typeaheadPrefix: prefix, + typeaheadContext: context, + typeaheadText: text, + }, + () => { + if (refresher) { + refresher.then(this.handleTypeahead).catch(e => console.error(e)); + } } - return group; - }); - - console.log('handleTypeahead', selection.anchorNode, wrapperClasses, text, offset, prefix, typeaheadContext); - - this.setState({ - typeaheadPrefix: prefix, - typeaheadContext, - typeaheadText: text, - suggestions: results > 0 ? filteredSuggestions : [], - }); + ); } }, TYPEAHEAD_DEBOUNCE); - applyTypeahead(change, suggestion) { - const { typeaheadPrefix, typeaheadContext, typeaheadText } = this.state; + applyTypeahead(change: Change, suggestion: Suggestion): Change { + const { cleanText, onWillApplySuggestion } = this.props; + const { typeaheadPrefix, typeaheadText } = this.state; + let suggestionText = suggestion.insertText || suggestion.label; + const move = suggestion.move || 0; - // Modify suggestion based on context - switch (typeaheadContext) { - case 'context-labels': { - const nextChar = getNextCharacter(); - if (!nextChar || nextChar === '}' || nextChar === ',') { - suggestion += '='; - } - break; - } - - case 'context-label-values': { - // Always add quotes and remove existing ones instead - if (!(typeaheadText.startsWith('="') || typeaheadText.startsWith('"'))) { - suggestion = `"${suggestion}`; - } - if (getNextCharacter() !== '"') { - suggestion = `${suggestion}"`; - } - break; - } - - default: + if (onWillApplySuggestion) { + suggestionText = onWillApplySuggestion(suggestionText, { ...this.state }); } this.resetTypeahead(); // Remove the current, incomplete text and replace it with the selected suggestion - let backward = typeaheadPrefix.length; - const text = cleanText(typeaheadText); + const backward = suggestion.deleteBackwards || typeaheadPrefix.length; + const text = cleanText ? cleanText(typeaheadText) : typeaheadText; const suffixLength = text.length - typeaheadPrefix.length; const offset = typeaheadText.indexOf(typeaheadPrefix); - const midWord = typeaheadPrefix && ((suffixLength > 0 && offset > -1) || suggestion === typeaheadText); + const midWord = typeaheadPrefix && ((suffixLength > 0 && offset > -1) || suggestionText === typeaheadText); const forward = midWord ? suffixLength + offset : 0; - return ( - change - // TODO this line breaks if cursor was moved left and length is longer than whole prefix + // If new-lines, apply suggestion as block + if (suggestionText.match(/\n/)) { + const fragment = makeFragment(suggestionText); + return change .deleteBackward(backward) .deleteForward(forward) - .insertText(suggestion) - .focus() - ); + .insertFragment(fragment) + .focus(); + } + + return change + .deleteBackward(backward) + .deleteForward(forward) + .insertText(suggestionText) + .move(move) + .focus(); } onKeyDown = (event, change) => { @@ -413,74 +379,6 @@ class QueryField extends React.Component { }); }; - async fetchLabelValues(key) { - const url = `/api/v1/label/${key}/values`; - try { - const res = await this.request(url); - console.log(res); - const body = await (res.data || res.json()); - const pairs = this.state.labelValues[EMPTY_METRIC]; - const values = { - ...pairs, - [key]: body.data, - }; - // const labelKeys = { - // ...this.state.labelKeys, - // [EMPTY_METRIC]: keys, - // }; - const labelValues = { - ...this.state.labelValues, - [EMPTY_METRIC]: values, - }; - this.setState({ labelValues }, this.handleTypeahead); - } catch (e) { - if (this.props.onRequestError) { - this.props.onRequestError(e); - } else { - console.error(e); - } - } - } - - async fetchMetricLabels(name) { - const url = `/api/v1/series?match[]=${name}`; - try { - const res = await this.request(url); - const body = await (res.data || res.json()); - const { keys, values } = processLabels(body.data); - const labelKeys = { - ...this.state.labelKeys, - [name]: keys, - }; - const labelValues = { - ...this.state.labelValues, - [name]: values, - }; - this.setState({ labelKeys, labelValues }, this.handleTypeahead); - } catch (e) { - if (this.props.onRequestError) { - this.props.onRequestError(e); - } else { - console.error(e); - } - } - } - - async fetchMetricNames() { - const url = '/api/v1/label/__name__/values'; - try { - const res = await this.request(url); - const body = await (res.data || res.json()); - this.setState({ metrics: body.data }, this.onMetricsReceived); - } catch (error) { - if (this.props.onRequestError) { - this.props.onRequestError(error); - } else { - console.error(error); - } - } - } - handleBlur = () => { const { onBlur } = this.props; // If we dont wait here, menu clicks wont work because the menu @@ -498,7 +396,7 @@ class QueryField extends React.Component { } }; - handleClickMenu = item => { + onClickMenu = (item: Suggestion) => { // Manually triggering change const change = this.applyTypeahead(this.state.value.change(), item); this.onChange(change); @@ -531,7 +429,7 @@ class QueryField extends React.Component { // Write DOM requestAnimationFrame(() => { - menu.style.opacity = 1; + menu.style.opacity = '1'; menu.style.top = `${rect.top + scrollY + rect.height + 4}px`; menu.style.left = `${rect.left + scrollX - 2}px`; }); @@ -554,17 +452,16 @@ class QueryField extends React.Component { let selectedIndex = Math.max(this.state.typeaheadIndex, 0); const flattenedSuggestions = flattenSuggestions(suggestions); selectedIndex = selectedIndex % flattenedSuggestions.length || 0; - const selectedKeys = (flattenedSuggestions.length > 0 ? [flattenedSuggestions[selectedIndex]] : []).map( - i => (typeof i === 'object' ? i.text : i) - ); + const selectedItem: Suggestion | null = + flattenedSuggestions.length > 0 ? flattenedSuggestions[selectedIndex] : null; // Create typeahead in DOM root so we can later position it absolutely return ( @@ -591,4 +488,24 @@ class QueryField extends React.Component { } } +class Portal extends React.Component<{ index?: number; prefix: string }, {}> { + node: HTMLElement; + + constructor(props) { + super(props); + const { index = 0, prefix = 'query' } = props; + this.node = document.createElement('div'); + this.node.classList.add(`slate-typeahead`, `slate-typeahead-${prefix}-${index}`); + document.body.appendChild(this.node); + } + + componentWillUnmount() { + document.body.removeChild(this.node); + } + + render() { + return ReactDOM.createPortal(this.props.children, this.node); + } +} + export default QueryField; diff --git a/public/app/containers/Explore/QueryRows.tsx b/public/app/containers/Explore/QueryRows.tsx index a968e1e2c64..3aaa006d6df 100644 --- a/public/app/containers/Explore/QueryRows.tsx +++ b/public/app/containers/Explore/QueryRows.tsx @@ -1,7 +1,6 @@ import React, { PureComponent } from 'react'; -import promql from './slate-plugins/prism/promql'; -import QueryField from './QueryField'; +import QueryField from './PromQueryField'; class QueryRow extends PureComponent { constructor(props) { @@ -62,9 +61,6 @@ class QueryRow extends PureComponent { portalPrefix="explore" onPressEnter={this.handlePressEnter} onQueryChange={this.handleChangeQuery} - placeholder="Enter a PromQL query" - prismLanguage="promql" - prismDefinition={promql} request={request} /> diff --git a/public/app/containers/Explore/Typeahead.tsx b/public/app/containers/Explore/Typeahead.tsx index 44fce7f8c7e..9924488035c 100644 --- a/public/app/containers/Explore/Typeahead.tsx +++ b/public/app/containers/Explore/Typeahead.tsx @@ -1,17 +1,26 @@ import React from 'react'; -function scrollIntoView(el) { +import { Suggestion, SuggestionGroup } from './QueryField'; + +function scrollIntoView(el: HTMLElement) { if (!el || !el.offsetParent) { return; } - const container = el.offsetParent; + const container = el.offsetParent as HTMLElement; if (el.offsetTop > container.scrollTop + container.offsetHeight || el.offsetTop < container.scrollTop) { container.scrollTop = el.offsetTop - container.offsetTop; } } -class TypeaheadItem extends React.PureComponent { - el: any; +interface TypeaheadItemProps { + isSelected: boolean; + item: Suggestion; + onClickItem: (Suggestion) => void; +} + +class TypeaheadItem extends React.PureComponent { + el: HTMLElement; + componentDidUpdate(prevProps) { if (this.props.isSelected && !prevProps.isSelected) { scrollIntoView(this.el); @@ -22,20 +31,30 @@ class TypeaheadItem extends React.PureComponent { this.el = el; }; + onClick = () => { + this.props.onClickItem(this.props.item); + }; + render() { - const { hint, isSelected, label, onClickItem } = this.props; + const { isSelected, item } = this.props; const className = isSelected ? 'typeahead-item typeahead-item__selected' : 'typeahead-item'; - const onClick = () => onClickItem(label); return ( -
  • - {label} - {hint && isSelected ?
    {hint}
    : null} +
  • + {item.detail || item.label} + {item.documentation && isSelected ?
    {item.documentation}
    : null}
  • ); } } -class TypeaheadGroup extends React.PureComponent { +interface TypeaheadGroupProps { + items: Suggestion[]; + label: string; + onClickItem: (Suggestion) => void; + selected: Suggestion; +} + +class TypeaheadGroup extends React.PureComponent { render() { const { items, label, selected, onClickItem } = this.props; return ( @@ -43,16 +62,8 @@ class TypeaheadGroup extends React.PureComponent {
    {label}
      {items.map(item => { - const text = typeof item === 'object' ? item.text : item; - const label = typeof item === 'object' ? item.display || item.text : item; return ( - -1} - hint={item.hint} - label={label} - /> + ); })}
    @@ -61,13 +72,19 @@ class TypeaheadGroup extends React.PureComponent { } } -class Typeahead extends React.PureComponent { +interface TypeaheadProps { + groupedItems: SuggestionGroup[]; + menuRef: any; + selectedItem: Suggestion | null; + onClickItem: (Suggestion) => void; +} +class Typeahead extends React.PureComponent { render() { - const { groupedItems, menuRef, selectedItems, onClickItem } = this.props; + const { groupedItems, menuRef, selectedItem, onClickItem } = this.props; return (
      {groupedItems.map(g => ( - + ))}
    ); diff --git a/public/app/containers/Explore/slate-plugins/prism/promql.ts b/public/app/containers/Explore/slate-plugins/prism/promql.ts index 0f0be18cb6f..a17c5fbc4f6 100644 --- a/public/app/containers/Explore/slate-plugins/prism/promql.ts +++ b/public/app/containers/Explore/slate-plugins/prism/promql.ts @@ -1,67 +1,368 @@ +/* tslint:disable max-line-length */ + export const OPERATORS = ['by', 'group_left', 'group_right', 'ignoring', 'on', 'offset', 'without']; const AGGREGATION_OPERATORS = [ - 'sum', - 'min', - 'max', - 'avg', - 'stddev', - 'stdvar', - 'count', - 'count_values', - 'bottomk', - 'topk', - 'quantile', + { + label: 'sum', + insertText: 'sum()', + documentation: 'Calculate sum over dimensions', + }, + { + label: 'min', + insertText: 'min()', + documentation: 'Select minimum over dimensions', + }, + { + label: 'max', + insertText: 'max()', + documentation: 'Select maximum over dimensions', + }, + { + label: 'avg', + insertText: 'avg()', + documentation: 'Calculate the average over dimensions', + }, + { + label: 'stddev', + insertText: 'stddev()', + documentation: 'Calculate population standard deviation over dimensions', + }, + { + label: 'stdvar', + insertText: 'stdvar()', + documentation: 'Calculate population standard variance over dimensions', + }, + { + label: 'count', + insertText: 'count()', + documentation: 'Count number of elements in the vector', + }, + { + label: 'count_values', + insertText: 'count_values()', + documentation: 'Count number of elements with the same value', + }, + { + label: 'bottomk', + insertText: 'bottomk()', + documentation: 'Smallest k elements by sample value', + }, + { + label: 'topk', + insertText: 'topk()', + documentation: 'Largest k elements by sample value', + }, + { + label: 'quantile', + insertText: 'quantile()', + documentation: 'Calculate φ-quantile (0 ≤ φ ≤ 1) over dimensions', + }, ]; export const FUNCTIONS = [ ...AGGREGATION_OPERATORS, - 'abs', - 'absent', - 'ceil', - 'changes', - 'clamp_max', - 'clamp_min', - 'count_scalar', - 'day_of_month', - 'day_of_week', - 'days_in_month', - 'delta', - 'deriv', - 'drop_common_labels', - 'exp', - 'floor', - 'histogram_quantile', - 'holt_winters', - 'hour', - 'idelta', - 'increase', - 'irate', - 'label_replace', - 'ln', - 'log2', - 'log10', - 'minute', - 'month', - 'predict_linear', - 'rate', - 'resets', - 'round', - 'scalar', - 'sort', - 'sort_desc', - 'sqrt', - 'time', - 'vector', - 'year', - 'avg_over_time', - 'min_over_time', - 'max_over_time', - 'sum_over_time', - 'count_over_time', - 'quantile_over_time', - 'stddev_over_time', - 'stdvar_over_time', + { + insertText: 'abs()', + label: 'abs', + detail: 'abs(v instant-vector)', + documentation: 'Returns the input vector with all sample values converted to their absolute value.', + }, + { + insertText: 'absent()', + label: 'absent', + detail: 'absent(v instant-vector)', + documentation: + 'Returns an empty vector if the vector passed to it has any elements and a 1-element vector with the value 1 if the vector passed to it has no elements. This is useful for alerting on when no time series exist for a given metric name and label combination.', + }, + { + insertText: 'ceil()', + label: 'ceil', + detail: 'ceil(v instant-vector)', + documentation: 'Rounds the sample values of all elements in `v` up to the nearest integer.', + }, + { + insertText: 'changes()', + label: 'changes', + detail: 'changes(v range-vector)', + documentation: + 'For each input time series, `changes(v range-vector)` returns the number of times its value has changed within the provided time range as an instant vector.', + }, + { + insertText: 'clamp_max()', + label: 'clamp_max', + detail: 'clamp_max(v instant-vector, max scalar)', + documentation: 'Clamps the sample values of all elements in `v` to have an upper limit of `max`.', + }, + { + insertText: 'clamp_min()', + label: 'clamp_min', + detail: 'clamp_min(v instant-vector, min scalar)', + documentation: 'Clamps the sample values of all elements in `v` to have a lower limit of `min`.', + }, + { + insertText: 'count_scalar()', + label: 'count_scalar', + detail: 'count_scalar(v instant-vector)', + documentation: + 'Returns the number of elements in a time series vector as a scalar. This is in contrast to the `count()` aggregation operator, which always returns a vector (an empty one if the input vector is empty) and allows grouping by labels via a `by` clause.', + }, + { + insertText: 'day_of_month()', + label: 'day_of_month', + detail: 'day_of_month(v=vector(time()) instant-vector)', + documentation: 'Returns the day of the month for each of the given times in UTC. Returned values are from 1 to 31.', + }, + { + insertText: 'day_of_week()', + label: 'day_of_week', + detail: 'day_of_week(v=vector(time()) instant-vector)', + documentation: + 'Returns the day of the week for each of the given times in UTC. Returned values are from 0 to 6, where 0 means Sunday etc.', + }, + { + insertText: 'days_in_month()', + label: 'days_in_month', + detail: 'days_in_month(v=vector(time()) instant-vector)', + documentation: + 'Returns number of days in the month for each of the given times in UTC. Returned values are from 28 to 31.', + }, + { + insertText: 'delta()', + label: 'delta', + detail: 'delta(v range-vector)', + documentation: + 'Calculates the difference between the first and last value of each time series element in a range vector `v`, returning an instant vector with the given deltas and equivalent labels. The delta is extrapolated to cover the full time range as specified in the range vector selector, so that it is possible to get a non-integer result even if the sample values are all integers.', + }, + { + insertText: 'deriv()', + label: 'deriv', + detail: 'deriv(v range-vector)', + documentation: + 'Calculates the per-second derivative of the time series in a range vector `v`, using simple linear regression.', + }, + { + insertText: 'drop_common_labels()', + label: 'drop_common_labels', + detail: 'drop_common_labels(instant-vector)', + documentation: 'Drops all labels that have the same name and value across all series in the input vector.', + }, + { + insertText: 'exp()', + label: 'exp', + detail: 'exp(v instant-vector)', + documentation: + 'Calculates the exponential function for all elements in `v`.\nSpecial cases are:\n* `Exp(+Inf) = +Inf` \n* `Exp(NaN) = NaN`', + }, + { + insertText: 'floor()', + label: 'floor', + detail: 'floor(v instant-vector)', + documentation: 'Rounds the sample values of all elements in `v` down to the nearest integer.', + }, + { + insertText: 'histogram_quantile()', + label: 'histogram_quantile', + detail: 'histogram_quantile(φ float, b instant-vector)', + documentation: + 'Calculates the φ-quantile (0 ≤ φ ≤ 1) from the buckets `b` of a histogram. The samples in `b` are the counts of observations in each bucket. Each sample must have a label `le` where the label value denotes the inclusive upper bound of the bucket. (Samples without such a label are silently ignored.) The histogram metric type automatically provides time series with the `_bucket` suffix and the appropriate labels.', + }, + { + insertText: 'holt_winters()', + label: 'holt_winters', + detail: 'holt_winters(v range-vector, sf scalar, tf scalar)', + documentation: + 'Produces a smoothed value for time series based on the range in `v`. The lower the smoothing factor `sf`, the more importance is given to old data. The higher the trend factor `tf`, the more trends in the data is considered. Both `sf` and `tf` must be between 0 and 1.', + }, + { + insertText: 'hour()', + label: 'hour', + detail: 'hour(v=vector(time()) instant-vector)', + documentation: 'Returns the hour of the day for each of the given times in UTC. Returned values are from 0 to 23.', + }, + { + insertText: 'idelta()', + label: 'idelta', + detail: 'idelta(v range-vector)', + documentation: + 'Calculates the difference between the last two samples in the range vector `v`, returning an instant vector with the given deltas and equivalent labels.', + }, + { + insertText: 'increase()', + label: 'increase', + detail: 'increase(v range-vector)', + documentation: + 'Calculates the increase in the time series in the range vector. Breaks in monotonicity (such as counter resets due to target restarts) are automatically adjusted for. The increase is extrapolated to cover the full time range as specified in the range vector selector, so that it is possible to get a non-integer result even if a counter increases only by integer increments.', + }, + { + insertText: 'irate()', + label: 'irate', + detail: 'irate(v range-vector)', + documentation: + 'Calculates the per-second instant rate of increase of the time series in the range vector. This is based on the last two data points. Breaks in monotonicity (such as counter resets due to target restarts) are automatically adjusted for.', + }, + { + insertText: 'label_replace()', + label: 'label_replace', + detail: 'label_replace(v instant-vector, dst_label string, replacement string, src_label string, regex string)', + documentation: + "For each timeseries in `v`, `label_replace(v instant-vector, dst_label string, replacement string, src_label string, regex string)` matches the regular expression `regex` against the label `src_label`. If it matches, then the timeseries is returned with the label `dst_label` replaced by the expansion of `replacement`. `$1` is replaced with the first matching subgroup, `$2` with the second etc. If the regular expression doesn't match then the timeseries is returned unchanged.", + }, + { + insertText: 'ln()', + label: 'ln', + detail: 'ln(v instant-vector)', + documentation: + 'calculates the natural logarithm for all elements in `v`.\nSpecial cases are:\n * `ln(+Inf) = +Inf`\n * `ln(0) = -Inf`\n * `ln(x < 0) = NaN`\n * `ln(NaN) = NaN`', + }, + { + insertText: 'log2()', + label: 'log2', + detail: 'log2(v instant-vector)', + documentation: + 'Calculates the binary logarithm for all elements in `v`. The special cases are equivalent to those in `ln`.', + }, + { + insertText: 'log10()', + label: 'log10', + detail: 'log10(v instant-vector)', + documentation: + 'Calculates the decimal logarithm for all elements in `v`. The special cases are equivalent to those in `ln`.', + }, + { + insertText: 'minute()', + label: 'minute', + detail: 'minute(v=vector(time()) instant-vector)', + documentation: + 'Returns the minute of the hour for each of the given times in UTC. Returned values are from 0 to 59.', + }, + { + insertText: 'month()', + label: 'month', + detail: 'month(v=vector(time()) instant-vector)', + documentation: + 'Returns the month of the year for each of the given times in UTC. Returned values are from 1 to 12, where 1 means January etc.', + }, + { + insertText: 'predict_linear()', + label: 'predict_linear', + detail: 'predict_linear(v range-vector, t scalar)', + documentation: + 'Predicts the value of time series `t` seconds from now, based on the range vector `v`, using simple linear regression.', + }, + { + insertText: 'rate()', + label: 'rate', + detail: 'rate(v range-vector)', + documentation: + "Calculates the per-second average rate of increase of the time series in the range vector. Breaks in monotonicity (such as counter resets due to target restarts) are automatically adjusted for. Also, the calculation extrapolates to the ends of the time range, allowing for missed scrapes or imperfect alignment of scrape cycles with the range's time period.", + }, + { + insertText: 'resets()', + label: 'resets', + detail: 'resets(v range-vector)', + documentation: + 'For each input time series, `resets(v range-vector)` returns the number of counter resets within the provided time range as an instant vector. Any decrease in the value between two consecutive samples is interpreted as a counter reset.', + }, + { + insertText: 'round()', + label: 'round', + detail: 'round(v instant-vector, to_nearest=1 scalar)', + documentation: + 'Rounds the sample values of all elements in `v` to the nearest integer. Ties are resolved by rounding up. The optional `to_nearest` argument allows specifying the nearest multiple to which the sample values should be rounded. This multiple may also be a fraction.', + }, + { + insertText: 'scalar()', + label: 'scalar', + detail: 'scalar(v instant-vector)', + documentation: + 'Given a single-element input vector, `scalar(v instant-vector)` returns the sample value of that single element as a scalar. If the input vector does not have exactly one element, `scalar` will return `NaN`.', + }, + { + insertText: 'sort()', + label: 'sort', + detail: 'sort(v instant-vector)', + documentation: 'Returns vector elements sorted by their sample values, in ascending order.', + }, + { + insertText: 'sort_desc()', + label: 'sort_desc', + detail: 'sort_desc(v instant-vector)', + documentation: 'Returns vector elements sorted by their sample values, in descending order.', + }, + { + insertText: 'sqrt()', + label: 'sqrt', + detail: 'sqrt(v instant-vector)', + documentation: 'Calculates the square root of all elements in `v`.', + }, + { + insertText: 'time()', + label: 'time', + detail: 'time()', + documentation: + 'Returns the number of seconds since January 1, 1970 UTC. Note that this does not actually return the current time, but the time at which the expression is to be evaluated.', + }, + { + insertText: 'vector()', + label: 'vector', + detail: 'vector(s scalar)', + documentation: 'Returns the scalar `s` as a vector with no labels.', + }, + { + insertText: 'year()', + label: 'year', + detail: 'year(v=vector(time()) instant-vector)', + documentation: 'Returns the year for each of the given times in UTC.', + }, + { + insertText: 'avg_over_time()', + label: 'avg_over_time', + detail: 'avg_over_time(range-vector)', + documentation: 'The average value of all points in the specified interval.', + }, + { + insertText: 'min_over_time()', + label: 'min_over_time', + detail: 'min_over_time(range-vector)', + documentation: 'The minimum value of all points in the specified interval.', + }, + { + insertText: 'max_over_time()', + label: 'max_over_time', + detail: 'max_over_time(range-vector)', + documentation: 'The maximum value of all points in the specified interval.', + }, + { + insertText: 'sum_over_time()', + label: 'sum_over_time', + detail: 'sum_over_time(range-vector)', + documentation: 'The sum of all values in the specified interval.', + }, + { + insertText: 'count_over_time()', + label: 'count_over_time', + detail: 'count_over_time(range-vector)', + documentation: 'The count of all values in the specified interval.', + }, + { + insertText: 'quantile_over_time()', + label: 'quantile_over_time', + detail: 'quantile_over_time(scalar, range-vector)', + documentation: 'The φ-quantile (0 ≤ φ ≤ 1) of the values in the specified interval.', + }, + { + insertText: 'stddev_over_time()', + label: 'stddev_over_time', + detail: 'stddev_over_time(range-vector)', + documentation: 'The population standard deviation of the values in the specified interval.', + }, + { + insertText: 'stdvar_over_time()', + label: 'stdvar_over_time', + detail: 'stdvar_over_time(range-vector)', + documentation: 'The population standard variance of the values in the specified interval.', + }, ]; const tokenizer = { @@ -93,7 +394,7 @@ const tokenizer = { }, }, }, - function: new RegExp(`\\b(?:${FUNCTIONS.join('|')})(?=\\s*\\()`, 'i'), + function: new RegExp(`\\b(?:${FUNCTIONS.map(f => f.label).join('|')})(?=\\s*\\()`, 'i'), 'context-range': [ { pattern: /\[[^\]]*(?=])/, // [1m] diff --git a/public/sass/components/_slate_editor.scss b/public/sass/components/_slate_editor.scss index 119c468292a..10b2238f4b8 100644 --- a/public/sass/components/_slate_editor.scss +++ b/public/sass/components/_slate_editor.scss @@ -71,6 +71,7 @@ .typeahead-item-hint { font-size: $font-size-xs; color: $text-color; + white-space: normal; } } } From fc06f8bfe71d758148708dee23c52af678935a52 Mon Sep 17 00:00:00 2001 From: Tobias Skarhed Date: Thu, 26 Jul 2018 17:22:15 +0200 Subject: [PATCH 172/786] Pass more tests --- public/app/plugins/panel/singlestat/module.ts | 1 + .../panel/singlestat/specs/singlestat.jest.ts | 34 ++++++++----------- 2 files changed, 15 insertions(+), 20 deletions(-) diff --git a/public/app/plugins/panel/singlestat/module.ts b/public/app/plugins/panel/singlestat/module.ts index ebd2628b086..7fafb5902d1 100644 --- a/public/app/plugins/panel/singlestat/module.ts +++ b/public/app/plugins/panel/singlestat/module.ts @@ -310,6 +310,7 @@ class SingleStatCtrl extends MetricsPanelCtrl { data.valueRounded = data.value; data.valueFormatted = formatFunc(data.value, this.dashboard.isTimezoneUtc()); } else { + console.log(lastPoint, lastValue); data.value = this.series[0].stats[this.panel.valueName]; data.flotpairs = this.series[0].flotpairs; diff --git a/public/app/plugins/panel/singlestat/specs/singlestat.jest.ts b/public/app/plugins/panel/singlestat/specs/singlestat.jest.ts index 2c945aa6eb2..7b89f86250c 100644 --- a/public/app/plugins/panel/singlestat/specs/singlestat.jest.ts +++ b/public/app/plugins/panel/singlestat/specs/singlestat.jest.ts @@ -7,7 +7,7 @@ import moment from 'moment'; describe('SingleStatCtrl', function() { let ctx = {}; let epoch = 1505826363746; - let clock; + Date.now = () => epoch; let $scope = { $on: () => {}, @@ -24,7 +24,7 @@ describe('SingleStatCtrl', function() { }, }; SingleStatCtrl.prototype.dashboard = { - isTimezoneUtc: () => {}, + isTimezoneUtc: jest.fn(() => true), }; function singleStatScenario(desc, func) { @@ -89,29 +89,30 @@ describe('SingleStatCtrl', function() { ctx.data = [{ target: 'test.cpu1', datapoints: [[10, 12], [20, 1505634997920]] }]; ctx.ctrl.panel.valueName = 'last_time'; ctx.ctrl.panel.format = 'dateTimeAsIso'; + ctx.ctrl.dashboard.isTimezoneUtc = () => false; }); it('Should use time instead of value', function() { - console.log(ctx.data.value); expect(ctx.data.value).toBe(1505634997920); expect(ctx.data.valueRounded).toBe(1505634997920); }); it('should set formatted value', function() { - expect(ctx.data.valueFormatted).toBe(moment(1505634997920).format('YYYY-MM-DD HH:mm:ss')); + expect(ctx.data.valueFormatted).toBe('2017-09-17 09:56:37'); }); }); singleStatScenario('showing last iso time instead of value (in UTC)', function(ctx) { ctx.setup(function() { - ctx.data = [{ target: 'test.cpu1', datapoints: [[10, 12], [20, 1505634997920]] }]; + ctx.data = [{ target: 'test.cpu1', datapoints: [[10, 12], [20, 5000]] }]; ctx.ctrl.panel.valueName = 'last_time'; ctx.ctrl.panel.format = 'dateTimeAsIso'; // ctx.setIsUtc(true); + ctx.ctrl.dashboard.isTimezoneUtc = () => true; }); - it('should set formatted value', function() { - expect(ctx.data.valueFormatted).toBe(moment.utc(1505634997920).format('YYYY-MM-DD HH:mm:ss')); + it('should set value', function() { + expect(ctx.data.valueFormatted).toBe('1970-01-01 00:00:05'); }); }); @@ -120,6 +121,7 @@ describe('SingleStatCtrl', function() { ctx.data = [{ target: 'test.cpu1', datapoints: [[10, 12], [20, 1505634997920]] }]; ctx.ctrl.panel.valueName = 'last_time'; ctx.ctrl.panel.format = 'dateTimeAsUS'; + ctx.ctrl.dashboard.isTimezoneUtc = () => false; }); it('Should use time instead of value', function() { @@ -134,21 +136,22 @@ describe('SingleStatCtrl', function() { singleStatScenario('showing last us time instead of value (in UTC)', function(ctx) { ctx.setup(function() { - ctx.data = [{ target: 'test.cpu1', datapoints: [[10, 12], [20, 1505634997920]] }]; + ctx.data = [{ target: 'test.cpu1', datapoints: [[10, 12], [20, 5000]] }]; ctx.ctrl.panel.valueName = 'last_time'; ctx.ctrl.panel.format = 'dateTimeAsUS'; // ctx.setIsUtc(true); + ctx.ctrl.dashboard.isTimezoneUtc = () => true; }); it('should set formatted value', function() { - expect(ctx.data.valueFormatted).toBe(moment.utc(1505634997920).format('MM/DD/YYYY h:mm:ss a')); + expect(ctx.data.valueFormatted).toBe('01/01/1970 12:00:05 am'); }); }); singleStatScenario('showing last time from now instead of value', function(ctx) { beforeEach(() => { // clock = sinon.useFakeTimers(epoch); - jest.useFakeTimers(); + //jest.useFakeTimers(); }); ctx.setup(function() { @@ -167,16 +170,11 @@ describe('SingleStatCtrl', function() { }); afterEach(() => { - jest.clearAllTimers(); + // jest.clearAllTimers(); }); }); singleStatScenario('showing last time from now instead of value (in UTC)', function(ctx) { - beforeEach(() => { - // clock = sinon.useFakeTimers(epoch); - jest.useFakeTimers(); - }); - ctx.setup(function() { ctx.data = [{ target: 'test.cpu1', datapoints: [[10, 12], [20, 1505634997920]] }]; ctx.ctrl.panel.valueName = 'last_time'; @@ -187,10 +185,6 @@ describe('SingleStatCtrl', function() { it('should set formatted value', function() { expect(ctx.data.valueFormatted).toBe('2 days ago'); }); - - afterEach(() => { - jest.clearAllTimers(); - }); }); singleStatScenario('MainValue should use same number for decimals as displayed when checking thresholds', function( From d42cea5d42c58175448986a8682b7a8c137be088 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Thu, 26 Jul 2018 18:09:42 +0200 Subject: [PATCH 173/786] refactor sql engine to make it hold all common code for sql datasources --- pkg/tsdb/sql_engine.go | 324 +++++++++++++++++++++++++++++++++++------ 1 file changed, 279 insertions(+), 45 deletions(-) diff --git a/pkg/tsdb/sql_engine.go b/pkg/tsdb/sql_engine.go index ec908aeb9de..9321e8912dc 100644 --- a/pkg/tsdb/sql_engine.go +++ b/pkg/tsdb/sql_engine.go @@ -1,11 +1,17 @@ package tsdb import ( + "container/list" "context" + "database/sql" "fmt" + "math" + "strings" "sync" "time" + "github.com/grafana/grafana/pkg/log" + "github.com/grafana/grafana/pkg/components/null" "github.com/go-xorm/core" @@ -14,27 +20,15 @@ import ( "github.com/grafana/grafana/pkg/models" ) -// SqlEngine is a wrapper class around xorm for relational database data sources. -type SqlEngine interface { - InitEngine(driverName string, dsInfo *models.DataSource, cnnstr string) error - Query( - ctx context.Context, - ds *models.DataSource, - query *TsdbQuery, - transformToTimeSeries func(query *Query, rows *core.Rows, result *QueryResult, tsdbQuery *TsdbQuery) error, - transformToTable func(query *Query, rows *core.Rows, result *QueryResult, tsdbQuery *TsdbQuery) error, - ) (*Response, error) -} - // SqlMacroEngine interpolates macros into sql. It takes in the Query to have access to query context and // timeRange to be able to generate queries that use from and to. type SqlMacroEngine interface { Interpolate(query *Query, timeRange *TimeRange, sql string) (string, error) } -type DefaultSqlEngine struct { - MacroEngine SqlMacroEngine - XormEngine *xorm.Engine +// SqlTableRowTransformer transforms a query result row to RowValues with proper types. +type SqlTableRowTransformer interface { + Transform(columnTypes []*sql.ColumnType, rows *core.Rows) (RowValues, error) } type engineCacheType struct { @@ -48,69 +42,92 @@ var engineCache = engineCacheType{ versions: make(map[int64]int), } -// InitEngine creates the db connection and inits the xorm engine or loads it from the engine cache -func (e *DefaultSqlEngine) InitEngine(driverName string, dsInfo *models.DataSource, cnnstr string) error { +var NewXormEngine = func(driverName string, connectionString string) (*xorm.Engine, error) { + return xorm.NewEngine(driverName, connectionString) +} + +type sqlQueryEndpoint struct { + macroEngine SqlMacroEngine + rowTransformer SqlTableRowTransformer + engine *xorm.Engine + timeColumnNames []string + metricColumnTypes []string + log log.Logger +} + +type SqlQueryEndpointConfiguration struct { + DriverName string + Datasource *models.DataSource + ConnectionString string + TimeColumnNames []string + MetricColumnTypes []string +} + +var NewSqlQueryEndpoint = func(config *SqlQueryEndpointConfiguration, rowTransformer SqlTableRowTransformer, macroEngine SqlMacroEngine, log log.Logger) (TsdbQueryEndpoint, error) { + queryEndpoint := sqlQueryEndpoint{ + rowTransformer: rowTransformer, + macroEngine: macroEngine, + timeColumnNames: []string{"time"}, + log: log, + } + + if len(config.TimeColumnNames) > 0 { + queryEndpoint.timeColumnNames = config.TimeColumnNames + } + engineCache.Lock() defer engineCache.Unlock() - if engine, present := engineCache.cache[dsInfo.Id]; present { - if version := engineCache.versions[dsInfo.Id]; version == dsInfo.Version { - e.XormEngine = engine - return nil + if engine, present := engineCache.cache[config.Datasource.Id]; present { + if version := engineCache.versions[config.Datasource.Id]; version == config.Datasource.Version { + queryEndpoint.engine = engine + return &queryEndpoint, nil } } - engine, err := xorm.NewEngine(driverName, cnnstr) + engine, err := NewXormEngine(config.DriverName, config.ConnectionString) if err != nil { - return err + return nil, err } engine.SetMaxOpenConns(10) engine.SetMaxIdleConns(10) - engineCache.versions[dsInfo.Id] = dsInfo.Version - engineCache.cache[dsInfo.Id] = engine - e.XormEngine = engine + engineCache.versions[config.Datasource.Id] = config.Datasource.Version + engineCache.cache[config.Datasource.Id] = engine + queryEndpoint.engine = engine - return nil + return &queryEndpoint, nil } -// Query is a default implementation of the Query method for an SQL data source. -// The caller of this function must implement transformToTimeSeries and transformToTable and -// pass them in as parameters. -func (e *DefaultSqlEngine) Query( - ctx context.Context, - dsInfo *models.DataSource, - tsdbQuery *TsdbQuery, - transformToTimeSeries func(query *Query, rows *core.Rows, result *QueryResult, tsdbQuery *TsdbQuery) error, - transformToTable func(query *Query, rows *core.Rows, result *QueryResult, tsdbQuery *TsdbQuery) error, -) (*Response, error) { +// Query is the main function for the SqlQueryEndpoint +func (e *sqlQueryEndpoint) Query(ctx context.Context, dsInfo *models.DataSource, tsdbQuery *TsdbQuery) (*Response, error) { result := &Response{ Results: make(map[string]*QueryResult), } - session := e.XormEngine.NewSession() + session := e.engine.NewSession() defer session.Close() db := session.DB() for _, query := range tsdbQuery.Queries { - rawSql := query.Model.Get("rawSql").MustString() - if rawSql == "" { + rawSQL := query.Model.Get("rawSql").MustString() + if rawSQL == "" { continue } queryResult := &QueryResult{Meta: simplejson.New(), RefId: query.RefId} result.Results[query.RefId] = queryResult - rawSql, err := e.MacroEngine.Interpolate(query, tsdbQuery.TimeRange, rawSql) + rawSQL, err := e.macroEngine.Interpolate(query, tsdbQuery.TimeRange, rawSQL) if err != nil { queryResult.Error = err continue } - queryResult.Meta.Set("sql", rawSql) + queryResult.Meta.Set("sql", rawSQL) - rows, err := db.Query(rawSql) + rows, err := db.Query(rawSQL) if err != nil { queryResult.Error = err continue @@ -122,13 +139,13 @@ func (e *DefaultSqlEngine) Query( switch format { case "time_series": - err := transformToTimeSeries(query, rows, queryResult, tsdbQuery) + err := e.transformToTimeSeries(query, rows, queryResult, tsdbQuery) if err != nil { queryResult.Error = err continue } case "table": - err := transformToTable(query, rows, queryResult, tsdbQuery) + err := e.transformToTable(query, rows, queryResult, tsdbQuery) if err != nil { queryResult.Error = err continue @@ -139,6 +156,223 @@ func (e *DefaultSqlEngine) Query( return result, nil } +func (e *sqlQueryEndpoint) transformToTable(query *Query, rows *core.Rows, result *QueryResult, tsdbQuery *TsdbQuery) error { + columnNames, err := rows.Columns() + columnCount := len(columnNames) + + if err != nil { + return err + } + + rowLimit := 1000000 + rowCount := 0 + timeIndex := -1 + + table := &Table{ + Columns: make([]TableColumn, columnCount), + Rows: make([]RowValues, 0), + } + + for i, name := range columnNames { + table.Columns[i].Text = name + + for _, tc := range e.timeColumnNames { + if name == tc { + timeIndex = i + break + } + } + } + + columnTypes, err := rows.ColumnTypes() + if err != nil { + return err + } + + for ; rows.Next(); rowCount++ { + if rowCount > rowLimit { + return fmt.Errorf("query row limit exceeded, limit %d", rowLimit) + } + + values, err := e.rowTransformer.Transform(columnTypes, rows) + if err != nil { + return err + } + + // converts column named time to unix timestamp in milliseconds + // to make native mssql datetime types and epoch dates work in + // annotation and table queries. + ConvertSqlTimeColumnToEpochMs(values, timeIndex) + table.Rows = append(table.Rows, values) + } + + result.Tables = append(result.Tables, table) + result.Meta.Set("rowCount", rowCount) + return nil +} + +func (e *sqlQueryEndpoint) transformToTimeSeries(query *Query, rows *core.Rows, result *QueryResult, tsdbQuery *TsdbQuery) error { + pointsBySeries := make(map[string]*TimeSeries) + seriesByQueryOrder := list.New() + + columnNames, err := rows.Columns() + if err != nil { + return err + } + + columnTypes, err := rows.ColumnTypes() + if err != nil { + return err + } + + rowLimit := 1000000 + rowCount := 0 + timeIndex := -1 + metricIndex := -1 + + // check columns of resultset: a column named time is mandatory + // the first text column is treated as metric name unless a column named metric is present + for i, col := range columnNames { + for _, tc := range e.timeColumnNames { + if col == tc { + timeIndex = i + continue + } + } + switch col { + case "metric": + metricIndex = i + default: + if metricIndex == -1 { + columnType := columnTypes[i].DatabaseTypeName() + + for _, mct := range e.metricColumnTypes { + if columnType == mct { + metricIndex = i + continue + } + } + } + } + } + + if timeIndex == -1 { + return fmt.Errorf("Found no column named %s", strings.Join(e.timeColumnNames, " or ")) + } + + fillMissing := query.Model.Get("fill").MustBool(false) + var fillInterval float64 + fillValue := null.Float{} + if fillMissing { + fillInterval = query.Model.Get("fillInterval").MustFloat64() * 1000 + if !query.Model.Get("fillNull").MustBool(false) { + fillValue.Float64 = query.Model.Get("fillValue").MustFloat64() + fillValue.Valid = true + } + } + + for rows.Next() { + var timestamp float64 + var value null.Float + var metric string + + if rowCount > rowLimit { + return fmt.Errorf("query row limit exceeded, limit %d", rowLimit) + } + + values, err := e.rowTransformer.Transform(columnTypes, rows) + if err != nil { + return err + } + + // converts column named time to unix timestamp in milliseconds to make + // native mysql datetime types and epoch dates work in + // annotation and table queries. + ConvertSqlTimeColumnToEpochMs(values, timeIndex) + + switch columnValue := values[timeIndex].(type) { + case int64: + timestamp = float64(columnValue) + case float64: + timestamp = columnValue + default: + return fmt.Errorf("Invalid type for column time, must be of type timestamp or unix timestamp, got: %T %v", columnValue, columnValue) + } + + if metricIndex >= 0 { + if columnValue, ok := values[metricIndex].(string); ok { + metric = columnValue + } else { + return fmt.Errorf("Column metric must be of type %s. metric column name: %s type: %s but datatype is %T", strings.Join(e.metricColumnTypes, ", "), columnNames[metricIndex], columnTypes[metricIndex].DatabaseTypeName(), values[metricIndex]) + } + } + + for i, col := range columnNames { + if i == timeIndex || i == metricIndex { + continue + } + + if value, err = ConvertSqlValueColumnToFloat(col, values[i]); err != nil { + return err + } + + if metricIndex == -1 { + metric = col + } + + series, exist := pointsBySeries[metric] + if !exist { + series = &TimeSeries{Name: metric} + pointsBySeries[metric] = series + seriesByQueryOrder.PushBack(metric) + } + + if fillMissing { + var intervalStart float64 + if !exist { + intervalStart = float64(tsdbQuery.TimeRange.MustGetFrom().UnixNano() / 1e6) + } else { + intervalStart = series.Points[len(series.Points)-1][1].Float64 + fillInterval + } + + // align interval start + intervalStart = math.Floor(intervalStart/fillInterval) * fillInterval + + for i := intervalStart; i < timestamp; i += fillInterval { + series.Points = append(series.Points, TimePoint{fillValue, null.FloatFrom(i)}) + rowCount++ + } + } + + series.Points = append(series.Points, TimePoint{value, null.FloatFrom(timestamp)}) + + e.log.Debug("Rows", "metric", metric, "time", timestamp, "value", value) + } + } + + for elem := seriesByQueryOrder.Front(); elem != nil; elem = elem.Next() { + key := elem.Value.(string) + result.Series = append(result.Series, pointsBySeries[key]) + + if fillMissing { + series := pointsBySeries[key] + // fill in values from last fetched value till interval end + intervalStart := series.Points[len(series.Points)-1][1].Float64 + intervalEnd := float64(tsdbQuery.TimeRange.MustGetTo().UnixNano() / 1e6) + + // align interval start + intervalStart = math.Floor(intervalStart/fillInterval) * fillInterval + for i := intervalStart + fillInterval; i < intervalEnd; i += fillInterval { + series.Points = append(series.Points, TimePoint{fillValue, null.FloatFrom(i)}) + rowCount++ + } + } + } + + result.Meta.Set("rowCount", rowCount) + return nil +} + // ConvertSqlTimeColumnToEpochMs converts column named time to unix timestamp in milliseconds // to make native datetime types and epoch dates work in annotation and table queries. func ConvertSqlTimeColumnToEpochMs(values RowValues, timeIndex int) { From 2f3851b915620040204919b17b603c5b07a7de1a Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Thu, 26 Jul 2018 18:10:17 +0200 Subject: [PATCH 174/786] postgres: use new sql engine --- pkg/tsdb/postgres/macros.go | 38 ++-- pkg/tsdb/postgres/macros_test.go | 2 +- pkg/tsdb/postgres/postgres.go | 269 +++-------------------------- pkg/tsdb/postgres/postgres_test.go | 30 ++-- 4 files changed, 64 insertions(+), 275 deletions(-) diff --git a/pkg/tsdb/postgres/macros.go b/pkg/tsdb/postgres/macros.go index 61e88418ff4..661dbf3d4ce 100644 --- a/pkg/tsdb/postgres/macros.go +++ b/pkg/tsdb/postgres/macros.go @@ -14,18 +14,18 @@ import ( const rsIdentifier = `([_a-zA-Z0-9]+)` const sExpr = `\$` + rsIdentifier + `\(([^\)]*)\)` -type PostgresMacroEngine struct { - TimeRange *tsdb.TimeRange - Query *tsdb.Query +type postgresMacroEngine struct { + timeRange *tsdb.TimeRange + query *tsdb.Query } -func NewPostgresMacroEngine() tsdb.SqlMacroEngine { - return &PostgresMacroEngine{} +func newPostgresMacroEngine() tsdb.SqlMacroEngine { + return &postgresMacroEngine{} } -func (m *PostgresMacroEngine) Interpolate(query *tsdb.Query, timeRange *tsdb.TimeRange, sql string) (string, error) { - m.TimeRange = timeRange - m.Query = query +func (m *postgresMacroEngine) Interpolate(query *tsdb.Query, timeRange *tsdb.TimeRange, sql string) (string, error) { + m.timeRange = timeRange + m.query = query rExp, _ := regexp.Compile(sExpr) var macroError error @@ -66,7 +66,7 @@ func replaceAllStringSubmatchFunc(re *regexp.Regexp, str string, repl func([]str return result + str[lastIndex:] } -func (m *PostgresMacroEngine) evaluateMacro(name string, args []string) (string, error) { +func (m *postgresMacroEngine) evaluateMacro(name string, args []string) (string, error) { switch name { case "__time": if len(args) == 0 { @@ -83,11 +83,11 @@ func (m *PostgresMacroEngine) evaluateMacro(name string, args []string) (string, return "", fmt.Errorf("missing time column argument for macro %v", name) } - return fmt.Sprintf("%s BETWEEN '%s' AND '%s'", args[0], m.TimeRange.GetFromAsTimeUTC().Format(time.RFC3339), m.TimeRange.GetToAsTimeUTC().Format(time.RFC3339)), nil + return fmt.Sprintf("%s BETWEEN '%s' AND '%s'", args[0], m.timeRange.GetFromAsTimeUTC().Format(time.RFC3339), m.timeRange.GetToAsTimeUTC().Format(time.RFC3339)), nil case "__timeFrom": - return fmt.Sprintf("'%s'", m.TimeRange.GetFromAsTimeUTC().Format(time.RFC3339)), nil + return fmt.Sprintf("'%s'", m.timeRange.GetFromAsTimeUTC().Format(time.RFC3339)), nil case "__timeTo": - return fmt.Sprintf("'%s'", m.TimeRange.GetToAsTimeUTC().Format(time.RFC3339)), nil + return fmt.Sprintf("'%s'", m.timeRange.GetToAsTimeUTC().Format(time.RFC3339)), nil case "__timeGroup": if len(args) < 2 { return "", fmt.Errorf("macro %v needs time column and interval and optional fill value", name) @@ -97,16 +97,16 @@ func (m *PostgresMacroEngine) evaluateMacro(name string, args []string) (string, return "", fmt.Errorf("error parsing interval %v", args[1]) } if len(args) == 3 { - m.Query.Model.Set("fill", true) - m.Query.Model.Set("fillInterval", interval.Seconds()) + m.query.Model.Set("fill", true) + m.query.Model.Set("fillInterval", interval.Seconds()) if args[2] == "NULL" { - m.Query.Model.Set("fillNull", true) + m.query.Model.Set("fillNull", true) } else { floatVal, err := strconv.ParseFloat(args[2], 64) if err != nil { return "", fmt.Errorf("error parsing fill value %v", args[2]) } - m.Query.Model.Set("fillValue", floatVal) + m.query.Model.Set("fillValue", floatVal) } } return fmt.Sprintf("floor(extract(epoch from %s)/%v)*%v AS time", args[0], interval.Seconds(), interval.Seconds()), nil @@ -114,11 +114,11 @@ func (m *PostgresMacroEngine) evaluateMacro(name string, args []string) (string, if len(args) == 0 { return "", fmt.Errorf("missing time column argument for macro %v", name) } - return fmt.Sprintf("%s >= %d AND %s <= %d", args[0], m.TimeRange.GetFromAsSecondsEpoch(), args[0], m.TimeRange.GetToAsSecondsEpoch()), nil + return fmt.Sprintf("%s >= %d AND %s <= %d", args[0], m.timeRange.GetFromAsSecondsEpoch(), args[0], m.timeRange.GetToAsSecondsEpoch()), nil case "__unixEpochFrom": - return fmt.Sprintf("%d", m.TimeRange.GetFromAsSecondsEpoch()), nil + return fmt.Sprintf("%d", m.timeRange.GetFromAsSecondsEpoch()), nil case "__unixEpochTo": - return fmt.Sprintf("%d", m.TimeRange.GetToAsSecondsEpoch()), nil + return fmt.Sprintf("%d", m.timeRange.GetToAsSecondsEpoch()), nil default: return "", fmt.Errorf("Unknown macro %v", name) } diff --git a/pkg/tsdb/postgres/macros_test.go b/pkg/tsdb/postgres/macros_test.go index 8c581850430..194573be0fd 100644 --- a/pkg/tsdb/postgres/macros_test.go +++ b/pkg/tsdb/postgres/macros_test.go @@ -12,7 +12,7 @@ import ( func TestMacroEngine(t *testing.T) { Convey("MacroEngine", t, func() { - engine := NewPostgresMacroEngine() + engine := newPostgresMacroEngine() query := &tsdb.Query{} Convey("Given a time range between 2018-04-12 00:00 and 2018-04-12 00:05", func() { diff --git a/pkg/tsdb/postgres/postgres.go b/pkg/tsdb/postgres/postgres.go index f19e4fb54f4..b9f333db127 100644 --- a/pkg/tsdb/postgres/postgres.go +++ b/pkg/tsdb/postgres/postgres.go @@ -1,46 +1,38 @@ package postgres import ( - "container/list" - "context" - "fmt" - "math" + "database/sql" "net/url" "strconv" "github.com/go-xorm/core" - "github.com/grafana/grafana/pkg/components/null" "github.com/grafana/grafana/pkg/log" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/tsdb" ) -type PostgresQueryEndpoint struct { - sqlEngine tsdb.SqlEngine - log log.Logger -} - func init() { - tsdb.RegisterTsdbQueryEndpoint("postgres", NewPostgresQueryEndpoint) + tsdb.RegisterTsdbQueryEndpoint("postgres", newPostgresQueryEndpoint) } -func NewPostgresQueryEndpoint(datasource *models.DataSource) (tsdb.TsdbQueryEndpoint, error) { - endpoint := &PostgresQueryEndpoint{ - log: log.New("tsdb.postgres"), - } - - endpoint.sqlEngine = &tsdb.DefaultSqlEngine{ - MacroEngine: NewPostgresMacroEngine(), - } +func newPostgresQueryEndpoint(datasource *models.DataSource) (tsdb.TsdbQueryEndpoint, error) { + logger := log.New("tsdb.postgres") cnnstr := generateConnectionString(datasource) - endpoint.log.Debug("getEngine", "connection", cnnstr) + logger.Debug("getEngine", "connection", cnnstr) - if err := endpoint.sqlEngine.InitEngine("postgres", datasource, cnnstr); err != nil { - return nil, err + config := tsdb.SqlQueryEndpointConfiguration{ + DriverName: "postgres", + ConnectionString: cnnstr, + Datasource: datasource, + MetricColumnTypes: []string{"UNKNOWN", "TEXT", "VARCHAR", "CHAR"}, } - return endpoint, nil + rowTransformer := postgresRowTransformer{ + log: logger, + } + + return tsdb.NewSqlQueryEndpoint(&config, &rowTransformer, newPostgresMacroEngine(), logger) } func generateConnectionString(datasource *models.DataSource) string { @@ -63,70 +55,15 @@ func generateConnectionString(datasource *models.DataSource) string { return u.String() } -func (e *PostgresQueryEndpoint) Query(ctx context.Context, dsInfo *models.DataSource, tsdbQuery *tsdb.TsdbQuery) (*tsdb.Response, error) { - return e.sqlEngine.Query(ctx, dsInfo, tsdbQuery, e.transformToTimeSeries, e.transformToTable) +type postgresRowTransformer struct { + log log.Logger } -func (e PostgresQueryEndpoint) transformToTable(query *tsdb.Query, rows *core.Rows, result *tsdb.QueryResult, tsdbQuery *tsdb.TsdbQuery) error { - columnNames, err := rows.Columns() - if err != nil { - return err - } +func (t *postgresRowTransformer) Transform(columnTypes []*sql.ColumnType, rows *core.Rows) (tsdb.RowValues, error) { + values := make([]interface{}, len(columnTypes)) + valuePtrs := make([]interface{}, len(columnTypes)) - table := &tsdb.Table{ - Columns: make([]tsdb.TableColumn, len(columnNames)), - Rows: make([]tsdb.RowValues, 0), - } - - for i, name := range columnNames { - table.Columns[i].Text = name - } - - rowLimit := 1000000 - rowCount := 0 - timeIndex := -1 - - // check if there is a column named time - for i, col := range columnNames { - switch col { - case "time": - timeIndex = i - } - } - - for ; rows.Next(); rowCount++ { - if rowCount > rowLimit { - return fmt.Errorf("PostgreSQL query row limit exceeded, limit %d", rowLimit) - } - - values, err := e.getTypedRowData(rows) - if err != nil { - return err - } - - // converts column named time to unix timestamp in milliseconds to make - // native postgres datetime types and epoch dates work in - // annotation and table queries. - tsdb.ConvertSqlTimeColumnToEpochMs(values, timeIndex) - - table.Rows = append(table.Rows, values) - } - - result.Tables = append(result.Tables, table) - result.Meta.Set("rowCount", rowCount) - return nil -} - -func (e PostgresQueryEndpoint) getTypedRowData(rows *core.Rows) (tsdb.RowValues, error) { - types, err := rows.ColumnTypes() - if err != nil { - return nil, err - } - - values := make([]interface{}, len(types)) - valuePtrs := make([]interface{}, len(types)) - - for i := 0; i < len(types); i++ { + for i := 0; i < len(columnTypes); i++ { valuePtrs[i] = &values[i] } @@ -136,20 +73,20 @@ func (e PostgresQueryEndpoint) getTypedRowData(rows *core.Rows) (tsdb.RowValues, // convert types not handled by lib/pq // unhandled types are returned as []byte - for i := 0; i < len(types); i++ { + for i := 0; i < len(columnTypes); i++ { if value, ok := values[i].([]byte); ok { - switch types[i].DatabaseTypeName() { + switch columnTypes[i].DatabaseTypeName() { case "NUMERIC": if v, err := strconv.ParseFloat(string(value), 64); err == nil { values[i] = v } else { - e.log.Debug("Rows", "Error converting numeric to float", value) + t.log.Debug("Rows", "Error converting numeric to float", value) } case "UNKNOWN", "CIDR", "INET", "MACADDR": // char literals have type UNKNOWN values[i] = string(value) default: - e.log.Debug("Rows", "Unknown database type", types[i].DatabaseTypeName(), "value", value) + t.log.Debug("Rows", "Unknown database type", columnTypes[i].DatabaseTypeName(), "value", value) values[i] = string(value) } } @@ -157,159 +94,3 @@ func (e PostgresQueryEndpoint) getTypedRowData(rows *core.Rows) (tsdb.RowValues, return values, nil } - -func (e PostgresQueryEndpoint) transformToTimeSeries(query *tsdb.Query, rows *core.Rows, result *tsdb.QueryResult, tsdbQuery *tsdb.TsdbQuery) error { - pointsBySeries := make(map[string]*tsdb.TimeSeries) - seriesByQueryOrder := list.New() - - columnNames, err := rows.Columns() - if err != nil { - return err - } - - columnTypes, err := rows.ColumnTypes() - if err != nil { - return err - } - - rowLimit := 1000000 - rowCount := 0 - timeIndex := -1 - metricIndex := -1 - - // check columns of resultset: a column named time is mandatory - // the first text column is treated as metric name unless a column named metric is present - for i, col := range columnNames { - switch col { - case "time": - timeIndex = i - case "metric": - metricIndex = i - default: - if metricIndex == -1 { - switch columnTypes[i].DatabaseTypeName() { - case "UNKNOWN", "TEXT", "VARCHAR", "CHAR": - metricIndex = i - } - } - } - } - - if timeIndex == -1 { - return fmt.Errorf("Found no column named time") - } - - fillMissing := query.Model.Get("fill").MustBool(false) - var fillInterval float64 - fillValue := null.Float{} - if fillMissing { - fillInterval = query.Model.Get("fillInterval").MustFloat64() * 1000 - if !query.Model.Get("fillNull").MustBool(false) { - fillValue.Float64 = query.Model.Get("fillValue").MustFloat64() - fillValue.Valid = true - } - } - - for rows.Next() { - var timestamp float64 - var value null.Float - var metric string - - if rowCount > rowLimit { - return fmt.Errorf("PostgreSQL query row limit exceeded, limit %d", rowLimit) - } - - values, err := e.getTypedRowData(rows) - if err != nil { - return err - } - - // converts column named time to unix timestamp in milliseconds to make - // native mysql datetime types and epoch dates work in - // annotation and table queries. - tsdb.ConvertSqlTimeColumnToEpochMs(values, timeIndex) - - switch columnValue := values[timeIndex].(type) { - case int64: - timestamp = float64(columnValue) - case float64: - timestamp = columnValue - default: - return fmt.Errorf("Invalid type for column time, must be of type timestamp or unix timestamp, got: %T %v", columnValue, columnValue) - } - - if metricIndex >= 0 { - if columnValue, ok := values[metricIndex].(string); ok { - metric = columnValue - } else { - return fmt.Errorf("Column metric must be of type char,varchar or text, got: %T %v", values[metricIndex], values[metricIndex]) - } - } - - for i, col := range columnNames { - if i == timeIndex || i == metricIndex { - continue - } - - if value, err = tsdb.ConvertSqlValueColumnToFloat(col, values[i]); err != nil { - return err - } - - if metricIndex == -1 { - metric = col - } - - series, exist := pointsBySeries[metric] - if !exist { - series = &tsdb.TimeSeries{Name: metric} - pointsBySeries[metric] = series - seriesByQueryOrder.PushBack(metric) - } - - if fillMissing { - var intervalStart float64 - if !exist { - intervalStart = float64(tsdbQuery.TimeRange.MustGetFrom().UnixNano() / 1e6) - } else { - intervalStart = series.Points[len(series.Points)-1][1].Float64 + fillInterval - } - - // align interval start - intervalStart = math.Floor(intervalStart/fillInterval) * fillInterval - - for i := intervalStart; i < timestamp; i += fillInterval { - series.Points = append(series.Points, tsdb.TimePoint{fillValue, null.FloatFrom(i)}) - rowCount++ - } - } - - series.Points = append(series.Points, tsdb.TimePoint{value, null.FloatFrom(timestamp)}) - - e.log.Debug("Rows", "metric", metric, "time", timestamp, "value", value) - rowCount++ - - } - } - - for elem := seriesByQueryOrder.Front(); elem != nil; elem = elem.Next() { - key := elem.Value.(string) - result.Series = append(result.Series, pointsBySeries[key]) - - if fillMissing { - series := pointsBySeries[key] - // fill in values from last fetched value till interval end - intervalStart := series.Points[len(series.Points)-1][1].Float64 - intervalEnd := float64(tsdbQuery.TimeRange.MustGetTo().UnixNano() / 1e6) - - // align interval start - intervalStart = math.Floor(intervalStart/fillInterval) * fillInterval - for i := intervalStart + fillInterval; i < intervalEnd; i += fillInterval { - series.Points = append(series.Points, tsdb.TimePoint{fillValue, null.FloatFrom(i)}) - rowCount++ - } - } - } - - result.Meta.Set("rowCount", rowCount) - return nil -} diff --git a/pkg/tsdb/postgres/postgres_test.go b/pkg/tsdb/postgres/postgres_test.go index a3a6d6546df..089829bf590 100644 --- a/pkg/tsdb/postgres/postgres_test.go +++ b/pkg/tsdb/postgres/postgres_test.go @@ -8,8 +8,9 @@ import ( "time" "github.com/go-xorm/xorm" + "github.com/grafana/grafana/pkg/components/securejsondata" "github.com/grafana/grafana/pkg/components/simplejson" - "github.com/grafana/grafana/pkg/log" + "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/sqlstore" "github.com/grafana/grafana/pkg/services/sqlstore/sqlutil" "github.com/grafana/grafana/pkg/tsdb" @@ -22,8 +23,9 @@ import ( // The tests require a PostgreSQL db named grafanadstest and a user/password grafanatest/grafanatest! // Use the docker/blocks/postgres_tests/docker-compose.yaml to spin up a // preconfigured Postgres server suitable for running these tests. -// There is also a dashboard.json in same directory that you can import to Grafana -// once you've created a datasource for the test server/database. +// There is also a datasource and dashboard provisioned by devenv scripts that you can +// use to verify that the generated data are vizualized as expected, see +// devenv/README.md for setup instructions. func TestPostgres(t *testing.T) { // change to true to run the MySQL tests runPostgresTests := false @@ -36,19 +38,25 @@ func TestPostgres(t *testing.T) { Convey("PostgreSQL", t, func() { x := InitPostgresTestDB(t) - endpoint := &PostgresQueryEndpoint{ - sqlEngine: &tsdb.DefaultSqlEngine{ - MacroEngine: NewPostgresMacroEngine(), - XormEngine: x, - }, - log: log.New("tsdb.postgres"), + origXormEngine := tsdb.NewXormEngine + tsdb.NewXormEngine = func(d, c string) (*xorm.Engine, error) { + return x, nil } - sess := x.NewSession() - defer sess.Close() + endpoint, err := newPostgresQueryEndpoint(&models.DataSource{ + JsonData: simplejson.New(), + SecureJsonData: securejsondata.SecureJsonData{}, + }) + So(err, ShouldBeNil) + sess := x.NewSession() fromStart := time.Date(2018, 3, 15, 13, 0, 0, 0, time.UTC).In(time.Local) + Reset(func() { + sess.Close() + tsdb.NewXormEngine = origXormEngine + }) + Convey("Given a table with different native data types", func() { sql := ` DROP TABLE IF EXISTS postgres_types; From 27db4540125ae1c5d342319fade4043bc2221081 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Thu, 26 Jul 2018 18:10:45 +0200 Subject: [PATCH 175/786] mysql: use new sql engine --- pkg/tsdb/mysql/macros.go | 38 ++--- pkg/tsdb/mysql/macros_test.go | 2 +- pkg/tsdb/mysql/mysql.go | 267 +++------------------------------- pkg/tsdb/mysql/mysql_test.go | 30 ++-- 4 files changed, 62 insertions(+), 275 deletions(-) diff --git a/pkg/tsdb/mysql/macros.go b/pkg/tsdb/mysql/macros.go index 584f731f3b8..078d1ff54f8 100644 --- a/pkg/tsdb/mysql/macros.go +++ b/pkg/tsdb/mysql/macros.go @@ -14,18 +14,18 @@ import ( const rsIdentifier = `([_a-zA-Z0-9]+)` const sExpr = `\$` + rsIdentifier + `\(([^\)]*)\)` -type MySqlMacroEngine struct { - TimeRange *tsdb.TimeRange - Query *tsdb.Query +type mySqlMacroEngine struct { + timeRange *tsdb.TimeRange + query *tsdb.Query } -func NewMysqlMacroEngine() tsdb.SqlMacroEngine { - return &MySqlMacroEngine{} +func newMysqlMacroEngine() tsdb.SqlMacroEngine { + return &mySqlMacroEngine{} } -func (m *MySqlMacroEngine) Interpolate(query *tsdb.Query, timeRange *tsdb.TimeRange, sql string) (string, error) { - m.TimeRange = timeRange - m.Query = query +func (m *mySqlMacroEngine) Interpolate(query *tsdb.Query, timeRange *tsdb.TimeRange, sql string) (string, error) { + m.timeRange = timeRange + m.query = query rExp, _ := regexp.Compile(sExpr) var macroError error @@ -66,7 +66,7 @@ func replaceAllStringSubmatchFunc(re *regexp.Regexp, str string, repl func([]str return result + str[lastIndex:] } -func (m *MySqlMacroEngine) evaluateMacro(name string, args []string) (string, error) { +func (m *mySqlMacroEngine) evaluateMacro(name string, args []string) (string, error) { switch name { case "__timeEpoch", "__time": if len(args) == 0 { @@ -78,11 +78,11 @@ func (m *MySqlMacroEngine) evaluateMacro(name string, args []string) (string, er return "", fmt.Errorf("missing time column argument for macro %v", name) } - return fmt.Sprintf("%s BETWEEN '%s' AND '%s'", args[0], m.TimeRange.GetFromAsTimeUTC().Format(time.RFC3339), m.TimeRange.GetToAsTimeUTC().Format(time.RFC3339)), nil + return fmt.Sprintf("%s BETWEEN '%s' AND '%s'", args[0], m.timeRange.GetFromAsTimeUTC().Format(time.RFC3339), m.timeRange.GetToAsTimeUTC().Format(time.RFC3339)), nil case "__timeFrom": - return fmt.Sprintf("'%s'", m.TimeRange.GetFromAsTimeUTC().Format(time.RFC3339)), nil + return fmt.Sprintf("'%s'", m.timeRange.GetFromAsTimeUTC().Format(time.RFC3339)), nil case "__timeTo": - return fmt.Sprintf("'%s'", m.TimeRange.GetToAsTimeUTC().Format(time.RFC3339)), nil + return fmt.Sprintf("'%s'", m.timeRange.GetToAsTimeUTC().Format(time.RFC3339)), nil case "__timeGroup": if len(args) < 2 { return "", fmt.Errorf("macro %v needs time column and interval", name) @@ -92,16 +92,16 @@ func (m *MySqlMacroEngine) evaluateMacro(name string, args []string) (string, er return "", fmt.Errorf("error parsing interval %v", args[1]) } if len(args) == 3 { - m.Query.Model.Set("fill", true) - m.Query.Model.Set("fillInterval", interval.Seconds()) + m.query.Model.Set("fill", true) + m.query.Model.Set("fillInterval", interval.Seconds()) if args[2] == "NULL" { - m.Query.Model.Set("fillNull", true) + m.query.Model.Set("fillNull", true) } else { floatVal, err := strconv.ParseFloat(args[2], 64) if err != nil { return "", fmt.Errorf("error parsing fill value %v", args[2]) } - m.Query.Model.Set("fillValue", floatVal) + m.query.Model.Set("fillValue", floatVal) } } return fmt.Sprintf("UNIX_TIMESTAMP(%s) DIV %.0f * %.0f", args[0], interval.Seconds(), interval.Seconds()), nil @@ -109,11 +109,11 @@ func (m *MySqlMacroEngine) evaluateMacro(name string, args []string) (string, er if len(args) == 0 { return "", fmt.Errorf("missing time column argument for macro %v", name) } - return fmt.Sprintf("%s >= %d AND %s <= %d", args[0], m.TimeRange.GetFromAsSecondsEpoch(), args[0], m.TimeRange.GetToAsSecondsEpoch()), nil + return fmt.Sprintf("%s >= %d AND %s <= %d", args[0], m.timeRange.GetFromAsSecondsEpoch(), args[0], m.timeRange.GetToAsSecondsEpoch()), nil case "__unixEpochFrom": - return fmt.Sprintf("%d", m.TimeRange.GetFromAsSecondsEpoch()), nil + return fmt.Sprintf("%d", m.timeRange.GetFromAsSecondsEpoch()), nil case "__unixEpochTo": - return fmt.Sprintf("%d", m.TimeRange.GetToAsSecondsEpoch()), nil + return fmt.Sprintf("%d", m.timeRange.GetToAsSecondsEpoch()), nil default: return "", fmt.Errorf("Unknown macro %v", name) } diff --git a/pkg/tsdb/mysql/macros_test.go b/pkg/tsdb/mysql/macros_test.go index 2561661b385..003af9a737f 100644 --- a/pkg/tsdb/mysql/macros_test.go +++ b/pkg/tsdb/mysql/macros_test.go @@ -12,7 +12,7 @@ import ( func TestMacroEngine(t *testing.T) { Convey("MacroEngine", t, func() { - engine := &MySqlMacroEngine{} + engine := &mySqlMacroEngine{} query := &tsdb.Query{} Convey("Given a time range between 2018-04-12 00:00 and 2018-04-12 00:05", func() { diff --git a/pkg/tsdb/mysql/mysql.go b/pkg/tsdb/mysql/mysql.go index 7eceaffdb09..645f6b49bbb 100644 --- a/pkg/tsdb/mysql/mysql.go +++ b/pkg/tsdb/mysql/mysql.go @@ -1,39 +1,24 @@ package mysql import ( - "container/list" - "context" "database/sql" "fmt" - "math" "reflect" "strconv" "github.com/go-sql-driver/mysql" "github.com/go-xorm/core" - "github.com/grafana/grafana/pkg/components/null" "github.com/grafana/grafana/pkg/log" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/tsdb" ) -type MysqlQueryEndpoint struct { - sqlEngine tsdb.SqlEngine - log log.Logger -} - func init() { - tsdb.RegisterTsdbQueryEndpoint("mysql", NewMysqlQueryEndpoint) + tsdb.RegisterTsdbQueryEndpoint("mysql", newMysqlQueryEndpoint) } -func NewMysqlQueryEndpoint(datasource *models.DataSource) (tsdb.TsdbQueryEndpoint, error) { - endpoint := &MysqlQueryEndpoint{ - log: log.New("tsdb.mysql"), - } - - endpoint.sqlEngine = &tsdb.DefaultSqlEngine{ - MacroEngine: NewMysqlMacroEngine(), - } +func newMysqlQueryEndpoint(datasource *models.DataSource) (tsdb.TsdbQueryEndpoint, error) { + logger := log.New("tsdb.mysql") cnnstr := fmt.Sprintf("%s:%s@%s(%s)/%s?collation=utf8mb4_unicode_ci&parseTime=true&loc=UTC&allowNativePasswords=true", datasource.User, @@ -42,85 +27,35 @@ func NewMysqlQueryEndpoint(datasource *models.DataSource) (tsdb.TsdbQueryEndpoin datasource.Url, datasource.Database, ) - endpoint.log.Debug("getEngine", "connection", cnnstr) + logger.Debug("getEngine", "connection", cnnstr) - if err := endpoint.sqlEngine.InitEngine("mysql", datasource, cnnstr); err != nil { - return nil, err + config := tsdb.SqlQueryEndpointConfiguration{ + DriverName: "mysql", + ConnectionString: cnnstr, + Datasource: datasource, + TimeColumnNames: []string{"time", "time_sec"}, + MetricColumnTypes: []string{"CHAR", "VARCHAR", "TINYTEXT", "TEXT", "MEDIUMTEXT", "LONGTEXT"}, } - return endpoint, nil + rowTransformer := mysqlRowTransformer{ + log: logger, + } + + return tsdb.NewSqlQueryEndpoint(&config, &rowTransformer, newMysqlMacroEngine(), logger) } -// Query is the main function for the MysqlExecutor -func (e *MysqlQueryEndpoint) Query(ctx context.Context, dsInfo *models.DataSource, tsdbQuery *tsdb.TsdbQuery) (*tsdb.Response, error) { - return e.sqlEngine.Query(ctx, dsInfo, tsdbQuery, e.transformToTimeSeries, e.transformToTable) +type mysqlRowTransformer struct { + log log.Logger } -func (e MysqlQueryEndpoint) transformToTable(query *tsdb.Query, rows *core.Rows, result *tsdb.QueryResult, tsdbQuery *tsdb.TsdbQuery) error { - columnNames, err := rows.Columns() - columnCount := len(columnNames) - - if err != nil { - return err - } - - table := &tsdb.Table{ - Columns: make([]tsdb.TableColumn, columnCount), - Rows: make([]tsdb.RowValues, 0), - } - - for i, name := range columnNames { - table.Columns[i].Text = name - } - - rowLimit := 1000000 - rowCount := 0 - timeIndex := -1 - - // check if there is a column named time - for i, col := range columnNames { - switch col { - case "time", "time_sec": - timeIndex = i - } - } - - for ; rows.Next(); rowCount++ { - if rowCount > rowLimit { - return fmt.Errorf("MySQL query row limit exceeded, limit %d", rowLimit) - } - - values, err := e.getTypedRowData(rows) - if err != nil { - return err - } - - // converts column named time to unix timestamp in milliseconds to make - // native mysql datetime types and epoch dates work in - // annotation and table queries. - tsdb.ConvertSqlTimeColumnToEpochMs(values, timeIndex) - - table.Rows = append(table.Rows, values) - } - - result.Tables = append(result.Tables, table) - result.Meta.Set("rowCount", rowCount) - return nil -} - -func (e MysqlQueryEndpoint) getTypedRowData(rows *core.Rows) (tsdb.RowValues, error) { - types, err := rows.ColumnTypes() - if err != nil { - return nil, err - } - - values := make([]interface{}, len(types)) +func (t *mysqlRowTransformer) Transform(columnTypes []*sql.ColumnType, rows *core.Rows) (tsdb.RowValues, error) { + values := make([]interface{}, len(columnTypes)) for i := range values { - scanType := types[i].ScanType() + scanType := columnTypes[i].ScanType() values[i] = reflect.New(scanType).Interface() - if types[i].DatabaseTypeName() == "BIT" { + if columnTypes[i].DatabaseTypeName() == "BIT" { values[i] = new([]byte) } } @@ -129,7 +64,7 @@ func (e MysqlQueryEndpoint) getTypedRowData(rows *core.Rows) (tsdb.RowValues, er return nil, err } - for i := 0; i < len(types); i++ { + for i := 0; i < len(columnTypes); i++ { typeName := reflect.ValueOf(values[i]).Type().String() switch typeName { @@ -158,7 +93,7 @@ func (e MysqlQueryEndpoint) getTypedRowData(rows *core.Rows) (tsdb.RowValues, er } } - if types[i].DatabaseTypeName() == "DECIMAL" { + if columnTypes[i].DatabaseTypeName() == "DECIMAL" { f, err := strconv.ParseFloat(values[i].(string), 64) if err == nil { @@ -171,159 +106,3 @@ func (e MysqlQueryEndpoint) getTypedRowData(rows *core.Rows) (tsdb.RowValues, er return values, nil } - -func (e MysqlQueryEndpoint) transformToTimeSeries(query *tsdb.Query, rows *core.Rows, result *tsdb.QueryResult, tsdbQuery *tsdb.TsdbQuery) error { - pointsBySeries := make(map[string]*tsdb.TimeSeries) - seriesByQueryOrder := list.New() - - columnNames, err := rows.Columns() - if err != nil { - return err - } - - columnTypes, err := rows.ColumnTypes() - if err != nil { - return err - } - - rowLimit := 1000000 - rowCount := 0 - timeIndex := -1 - metricIndex := -1 - - // check columns of resultset: a column named time is mandatory - // the first text column is treated as metric name unless a column named metric is present - for i, col := range columnNames { - switch col { - case "time", "time_sec": - timeIndex = i - case "metric": - metricIndex = i - default: - if metricIndex == -1 { - switch columnTypes[i].DatabaseTypeName() { - case "CHAR", "VARCHAR", "TINYTEXT", "TEXT", "MEDIUMTEXT", "LONGTEXT": - metricIndex = i - } - } - } - } - - if timeIndex == -1 { - return fmt.Errorf("Found no column named time or time_sec") - } - - fillMissing := query.Model.Get("fill").MustBool(false) - var fillInterval float64 - fillValue := null.Float{} - if fillMissing { - fillInterval = query.Model.Get("fillInterval").MustFloat64() * 1000 - if !query.Model.Get("fillNull").MustBool(false) { - fillValue.Float64 = query.Model.Get("fillValue").MustFloat64() - fillValue.Valid = true - } - } - - for rows.Next() { - var timestamp float64 - var value null.Float - var metric string - - if rowCount > rowLimit { - return fmt.Errorf("PostgreSQL query row limit exceeded, limit %d", rowLimit) - } - - values, err := e.getTypedRowData(rows) - if err != nil { - return err - } - - // converts column named time to unix timestamp in milliseconds to make - // native mysql datetime types and epoch dates work in - // annotation and table queries. - tsdb.ConvertSqlTimeColumnToEpochMs(values, timeIndex) - - switch columnValue := values[timeIndex].(type) { - case int64: - timestamp = float64(columnValue) - case float64: - timestamp = columnValue - default: - return fmt.Errorf("Invalid type for column time/time_sec, must be of type timestamp or unix timestamp, got: %T %v", columnValue, columnValue) - } - - if metricIndex >= 0 { - if columnValue, ok := values[metricIndex].(string); ok { - metric = columnValue - } else { - return fmt.Errorf("Column metric must be of type char,varchar or text, got: %T %v", values[metricIndex], values[metricIndex]) - } - } - - for i, col := range columnNames { - if i == timeIndex || i == metricIndex { - continue - } - - if value, err = tsdb.ConvertSqlValueColumnToFloat(col, values[i]); err != nil { - return err - } - - if metricIndex == -1 { - metric = col - } - - series, exist := pointsBySeries[metric] - if !exist { - series = &tsdb.TimeSeries{Name: metric} - pointsBySeries[metric] = series - seriesByQueryOrder.PushBack(metric) - } - - if fillMissing { - var intervalStart float64 - if !exist { - intervalStart = float64(tsdbQuery.TimeRange.MustGetFrom().UnixNano() / 1e6) - } else { - intervalStart = series.Points[len(series.Points)-1][1].Float64 + fillInterval - } - - // align interval start - intervalStart = math.Floor(intervalStart/fillInterval) * fillInterval - - for i := intervalStart; i < timestamp; i += fillInterval { - series.Points = append(series.Points, tsdb.TimePoint{fillValue, null.FloatFrom(i)}) - rowCount++ - } - } - - series.Points = append(series.Points, tsdb.TimePoint{value, null.FloatFrom(timestamp)}) - - e.log.Debug("Rows", "metric", metric, "time", timestamp, "value", value) - rowCount++ - - } - } - - for elem := seriesByQueryOrder.Front(); elem != nil; elem = elem.Next() { - key := elem.Value.(string) - result.Series = append(result.Series, pointsBySeries[key]) - - if fillMissing { - series := pointsBySeries[key] - // fill in values from last fetched value till interval end - intervalStart := series.Points[len(series.Points)-1][1].Float64 - intervalEnd := float64(tsdbQuery.TimeRange.MustGetTo().UnixNano() / 1e6) - - // align interval start - intervalStart = math.Floor(intervalStart/fillInterval) * fillInterval - for i := intervalStart + fillInterval; i < intervalEnd; i += fillInterval { - series.Points = append(series.Points, tsdb.TimePoint{fillValue, null.FloatFrom(i)}) - rowCount++ - } - } - } - - result.Meta.Set("rowCount", rowCount) - return nil -} diff --git a/pkg/tsdb/mysql/mysql_test.go b/pkg/tsdb/mysql/mysql_test.go index 850a37617e2..3b4e283b726 100644 --- a/pkg/tsdb/mysql/mysql_test.go +++ b/pkg/tsdb/mysql/mysql_test.go @@ -8,8 +8,9 @@ import ( "time" "github.com/go-xorm/xorm" + "github.com/grafana/grafana/pkg/components/securejsondata" "github.com/grafana/grafana/pkg/components/simplejson" - "github.com/grafana/grafana/pkg/log" + "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/sqlstore" "github.com/grafana/grafana/pkg/services/sqlstore/sqlutil" "github.com/grafana/grafana/pkg/tsdb" @@ -21,8 +22,9 @@ import ( // The tests require a MySQL db named grafana_ds_tests and a user/password grafana/password // Use the docker/blocks/mysql_tests/docker-compose.yaml to spin up a // preconfigured MySQL server suitable for running these tests. -// There is also a dashboard.json in same directory that you can import to Grafana -// once you've created a datasource for the test server/database. +// There is also a datasource and dashboard provisioned by devenv scripts that you can +// use to verify that the generated data are vizualized as expected, see +// devenv/README.md for setup instructions. func TestMySQL(t *testing.T) { // change to true to run the MySQL tests runMySqlTests := false @@ -35,19 +37,25 @@ func TestMySQL(t *testing.T) { Convey("MySQL", t, func() { x := InitMySQLTestDB(t) - endpoint := &MysqlQueryEndpoint{ - sqlEngine: &tsdb.DefaultSqlEngine{ - MacroEngine: NewMysqlMacroEngine(), - XormEngine: x, - }, - log: log.New("tsdb.mysql"), + origXormEngine := tsdb.NewXormEngine + tsdb.NewXormEngine = func(d, c string) (*xorm.Engine, error) { + return x, nil } - sess := x.NewSession() - defer sess.Close() + endpoint, err := newMysqlQueryEndpoint(&models.DataSource{ + JsonData: simplejson.New(), + SecureJsonData: securejsondata.SecureJsonData{}, + }) + So(err, ShouldBeNil) + sess := x.NewSession() fromStart := time.Date(2018, 3, 15, 13, 0, 0, 0, time.UTC) + Reset(func() { + sess.Close() + tsdb.NewXormEngine = origXormEngine + }) + Convey("Given a table with different native data types", func() { if exists, err := sess.IsTableExist("mysql_types"); err != nil || exists { So(err, ShouldBeNil) From 4f7882cda2b3443e473caf426a321841b223a8ab Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Thu, 26 Jul 2018 18:11:10 +0200 Subject: [PATCH 176/786] mssql: use new sql engine --- pkg/tsdb/mssql/macros.go | 38 ++--- pkg/tsdb/mssql/macros_test.go | 2 +- pkg/tsdb/mssql/mssql.go | 268 ++++------------------------------ pkg/tsdb/mssql/mssql_test.go | 30 ++-- 4 files changed, 64 insertions(+), 274 deletions(-) diff --git a/pkg/tsdb/mssql/macros.go b/pkg/tsdb/mssql/macros.go index ad3d1edd5d7..2c16b5cb27f 100644 --- a/pkg/tsdb/mssql/macros.go +++ b/pkg/tsdb/mssql/macros.go @@ -14,18 +14,18 @@ import ( const rsIdentifier = `([_a-zA-Z0-9]+)` const sExpr = `\$` + rsIdentifier + `\(([^\)]*)\)` -type MsSqlMacroEngine struct { - TimeRange *tsdb.TimeRange - Query *tsdb.Query +type msSqlMacroEngine struct { + timeRange *tsdb.TimeRange + query *tsdb.Query } -func NewMssqlMacroEngine() tsdb.SqlMacroEngine { - return &MsSqlMacroEngine{} +func newMssqlMacroEngine() tsdb.SqlMacroEngine { + return &msSqlMacroEngine{} } -func (m *MsSqlMacroEngine) Interpolate(query *tsdb.Query, timeRange *tsdb.TimeRange, sql string) (string, error) { - m.TimeRange = timeRange - m.Query = query +func (m *msSqlMacroEngine) Interpolate(query *tsdb.Query, timeRange *tsdb.TimeRange, sql string) (string, error) { + m.timeRange = timeRange + m.query = query rExp, _ := regexp.Compile(sExpr) var macroError error @@ -66,7 +66,7 @@ func replaceAllStringSubmatchFunc(re *regexp.Regexp, str string, repl func([]str return result + str[lastIndex:] } -func (m *MsSqlMacroEngine) evaluateMacro(name string, args []string) (string, error) { +func (m *msSqlMacroEngine) evaluateMacro(name string, args []string) (string, error) { switch name { case "__time": if len(args) == 0 { @@ -83,11 +83,11 @@ func (m *MsSqlMacroEngine) evaluateMacro(name string, args []string) (string, er return "", fmt.Errorf("missing time column argument for macro %v", name) } - return fmt.Sprintf("%s BETWEEN '%s' AND '%s'", args[0], m.TimeRange.GetFromAsTimeUTC().Format(time.RFC3339), m.TimeRange.GetToAsTimeUTC().Format(time.RFC3339)), nil + return fmt.Sprintf("%s BETWEEN '%s' AND '%s'", args[0], m.timeRange.GetFromAsTimeUTC().Format(time.RFC3339), m.timeRange.GetToAsTimeUTC().Format(time.RFC3339)), nil case "__timeFrom": - return fmt.Sprintf("'%s'", m.TimeRange.GetFromAsTimeUTC().Format(time.RFC3339)), nil + return fmt.Sprintf("'%s'", m.timeRange.GetFromAsTimeUTC().Format(time.RFC3339)), nil case "__timeTo": - return fmt.Sprintf("'%s'", m.TimeRange.GetToAsTimeUTC().Format(time.RFC3339)), nil + return fmt.Sprintf("'%s'", m.timeRange.GetToAsTimeUTC().Format(time.RFC3339)), nil case "__timeGroup": if len(args) < 2 { return "", fmt.Errorf("macro %v needs time column and interval", name) @@ -97,16 +97,16 @@ func (m *MsSqlMacroEngine) evaluateMacro(name string, args []string) (string, er return "", fmt.Errorf("error parsing interval %v", args[1]) } if len(args) == 3 { - m.Query.Model.Set("fill", true) - m.Query.Model.Set("fillInterval", interval.Seconds()) + m.query.Model.Set("fill", true) + m.query.Model.Set("fillInterval", interval.Seconds()) if args[2] == "NULL" { - m.Query.Model.Set("fillNull", true) + m.query.Model.Set("fillNull", true) } else { floatVal, err := strconv.ParseFloat(args[2], 64) if err != nil { return "", fmt.Errorf("error parsing fill value %v", args[2]) } - m.Query.Model.Set("fillValue", floatVal) + m.query.Model.Set("fillValue", floatVal) } } return fmt.Sprintf("FLOOR(DATEDIFF(second, '1970-01-01', %s)/%.0f)*%.0f", args[0], interval.Seconds(), interval.Seconds()), nil @@ -114,11 +114,11 @@ func (m *MsSqlMacroEngine) evaluateMacro(name string, args []string) (string, er if len(args) == 0 { return "", fmt.Errorf("missing time column argument for macro %v", name) } - return fmt.Sprintf("%s >= %d AND %s <= %d", args[0], m.TimeRange.GetFromAsSecondsEpoch(), args[0], m.TimeRange.GetToAsSecondsEpoch()), nil + return fmt.Sprintf("%s >= %d AND %s <= %d", args[0], m.timeRange.GetFromAsSecondsEpoch(), args[0], m.timeRange.GetToAsSecondsEpoch()), nil case "__unixEpochFrom": - return fmt.Sprintf("%d", m.TimeRange.GetFromAsSecondsEpoch()), nil + return fmt.Sprintf("%d", m.timeRange.GetFromAsSecondsEpoch()), nil case "__unixEpochTo": - return fmt.Sprintf("%d", m.TimeRange.GetToAsSecondsEpoch()), nil + return fmt.Sprintf("%d", m.timeRange.GetToAsSecondsEpoch()), nil default: return "", fmt.Errorf("Unknown macro %v", name) } diff --git a/pkg/tsdb/mssql/macros_test.go b/pkg/tsdb/mssql/macros_test.go index 49368fe3631..1895cd99442 100644 --- a/pkg/tsdb/mssql/macros_test.go +++ b/pkg/tsdb/mssql/macros_test.go @@ -14,7 +14,7 @@ import ( func TestMacroEngine(t *testing.T) { Convey("MacroEngine", t, func() { - engine := &MsSqlMacroEngine{} + engine := &msSqlMacroEngine{} query := &tsdb.Query{ Model: simplejson.New(), } diff --git a/pkg/tsdb/mssql/mssql.go b/pkg/tsdb/mssql/mssql.go index eb71259b46b..72e57d03fa0 100644 --- a/pkg/tsdb/mssql/mssql.go +++ b/pkg/tsdb/mssql/mssql.go @@ -1,49 +1,40 @@ package mssql import ( - "container/list" - "context" "database/sql" "fmt" "strconv" "strings" - "math" - _ "github.com/denisenkom/go-mssqldb" "github.com/go-xorm/core" - "github.com/grafana/grafana/pkg/components/null" "github.com/grafana/grafana/pkg/log" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/tsdb" ) -type MssqlQueryEndpoint struct { - sqlEngine tsdb.SqlEngine - log log.Logger -} - func init() { - tsdb.RegisterTsdbQueryEndpoint("mssql", NewMssqlQueryEndpoint) + tsdb.RegisterTsdbQueryEndpoint("mssql", newMssqlQueryEndpoint) } -func NewMssqlQueryEndpoint(datasource *models.DataSource) (tsdb.TsdbQueryEndpoint, error) { - endpoint := &MssqlQueryEndpoint{ - log: log.New("tsdb.mssql"), - } - - endpoint.sqlEngine = &tsdb.DefaultSqlEngine{ - MacroEngine: NewMssqlMacroEngine(), - } +func newMssqlQueryEndpoint(datasource *models.DataSource) (tsdb.TsdbQueryEndpoint, error) { + logger := log.New("tsdb.mssql") cnnstr := generateConnectionString(datasource) - endpoint.log.Debug("getEngine", "connection", cnnstr) + logger.Debug("getEngine", "connection", cnnstr) - if err := endpoint.sqlEngine.InitEngine("mssql", datasource, cnnstr); err != nil { - return nil, err + config := tsdb.SqlQueryEndpointConfiguration{ + DriverName: "mssql", + ConnectionString: cnnstr, + Datasource: datasource, + MetricColumnTypes: []string{"VARCHAR", "CHAR", "NVARCHAR", "NCHAR"}, } - return endpoint, nil + rowTransformer := mssqlRowTransformer{ + log: logger, + } + + return tsdb.NewSqlQueryEndpoint(&config, &rowTransformer, newMssqlMacroEngine(), logger) } func generateConnectionString(datasource *models.DataSource) string { @@ -70,71 +61,16 @@ func generateConnectionString(datasource *models.DataSource) string { ) } -// Query is the main function for the MssqlQueryEndpoint -func (e *MssqlQueryEndpoint) Query(ctx context.Context, dsInfo *models.DataSource, tsdbQuery *tsdb.TsdbQuery) (*tsdb.Response, error) { - return e.sqlEngine.Query(ctx, dsInfo, tsdbQuery, e.transformToTimeSeries, e.transformToTable) +type mssqlRowTransformer struct { + log log.Logger } -func (e MssqlQueryEndpoint) transformToTable(query *tsdb.Query, rows *core.Rows, result *tsdb.QueryResult, tsdbQuery *tsdb.TsdbQuery) error { - columnNames, err := rows.Columns() - columnCount := len(columnNames) +func (t *mssqlRowTransformer) Transform(columnTypes []*sql.ColumnType, rows *core.Rows) (tsdb.RowValues, error) { + values := make([]interface{}, len(columnTypes)) + valuePtrs := make([]interface{}, len(columnTypes)) - if err != nil { - return err - } - - rowLimit := 1000000 - rowCount := 0 - timeIndex := -1 - - table := &tsdb.Table{ - Columns: make([]tsdb.TableColumn, columnCount), - Rows: make([]tsdb.RowValues, 0), - } - - for i, name := range columnNames { - table.Columns[i].Text = name - - // check if there is a column named time - switch name { - case "time": - timeIndex = i - } - } - - columnTypes, err := rows.ColumnTypes() - if err != nil { - return err - } - - for ; rows.Next(); rowCount++ { - if rowCount > rowLimit { - return fmt.Errorf("MsSQL query row limit exceeded, limit %d", rowLimit) - } - - values, err := e.getTypedRowData(columnTypes, rows) - if err != nil { - return err - } - - // converts column named time to unix timestamp in milliseconds - // to make native mssql datetime types and epoch dates work in - // annotation and table queries. - tsdb.ConvertSqlTimeColumnToEpochMs(values, timeIndex) - table.Rows = append(table.Rows, values) - } - - result.Tables = append(result.Tables, table) - result.Meta.Set("rowCount", rowCount) - return nil -} - -func (e MssqlQueryEndpoint) getTypedRowData(types []*sql.ColumnType, rows *core.Rows) (tsdb.RowValues, error) { - values := make([]interface{}, len(types)) - valuePtrs := make([]interface{}, len(types)) - - for i, stype := range types { - e.log.Debug("type", "type", stype) + for i, stype := range columnTypes { + t.log.Debug("type", "type", stype) valuePtrs[i] = &values[i] } @@ -144,17 +80,17 @@ func (e MssqlQueryEndpoint) getTypedRowData(types []*sql.ColumnType, rows *core. // convert types not handled by denisenkom/go-mssqldb // unhandled types are returned as []byte - for i := 0; i < len(types); i++ { + for i := 0; i < len(columnTypes); i++ { if value, ok := values[i].([]byte); ok { - switch types[i].DatabaseTypeName() { + switch columnTypes[i].DatabaseTypeName() { case "MONEY", "SMALLMONEY", "DECIMAL": if v, err := strconv.ParseFloat(string(value), 64); err == nil { values[i] = v } else { - e.log.Debug("Rows", "Error converting numeric to float", value) + t.log.Debug("Rows", "Error converting numeric to float", value) } default: - e.log.Debug("Rows", "Unknown database type", types[i].DatabaseTypeName(), "value", value) + t.log.Debug("Rows", "Unknown database type", columnTypes[i].DatabaseTypeName(), "value", value) values[i] = string(value) } } @@ -162,157 +98,3 @@ func (e MssqlQueryEndpoint) getTypedRowData(types []*sql.ColumnType, rows *core. return values, nil } - -func (e MssqlQueryEndpoint) transformToTimeSeries(query *tsdb.Query, rows *core.Rows, result *tsdb.QueryResult, tsdbQuery *tsdb.TsdbQuery) error { - pointsBySeries := make(map[string]*tsdb.TimeSeries) - seriesByQueryOrder := list.New() - - columnNames, err := rows.Columns() - if err != nil { - return err - } - - columnTypes, err := rows.ColumnTypes() - if err != nil { - return err - } - - rowLimit := 1000000 - rowCount := 0 - timeIndex := -1 - metricIndex := -1 - - // check columns of resultset: a column named time is mandatory - // the first text column is treated as metric name unless a column named metric is present - for i, col := range columnNames { - switch col { - case "time": - timeIndex = i - case "metric": - metricIndex = i - default: - if metricIndex == -1 { - switch columnTypes[i].DatabaseTypeName() { - case "VARCHAR", "CHAR", "NVARCHAR", "NCHAR": - metricIndex = i - } - } - } - } - - if timeIndex == -1 { - return fmt.Errorf("Found no column named time") - } - - fillMissing := query.Model.Get("fill").MustBool(false) - var fillInterval float64 - fillValue := null.Float{} - if fillMissing { - fillInterval = query.Model.Get("fillInterval").MustFloat64() * 1000 - if !query.Model.Get("fillNull").MustBool(false) { - fillValue.Float64 = query.Model.Get("fillValue").MustFloat64() - fillValue.Valid = true - } - } - - for rows.Next() { - var timestamp float64 - var value null.Float - var metric string - - if rowCount > rowLimit { - return fmt.Errorf("MSSQL query row limit exceeded, limit %d", rowLimit) - } - - values, err := e.getTypedRowData(columnTypes, rows) - if err != nil { - return err - } - - // converts column named time to unix timestamp in milliseconds to make - // native mysql datetime types and epoch dates work in - // annotation and table queries. - tsdb.ConvertSqlTimeColumnToEpochMs(values, timeIndex) - - switch columnValue := values[timeIndex].(type) { - case int64: - timestamp = float64(columnValue) - case float64: - timestamp = columnValue - default: - return fmt.Errorf("Invalid type for column time, must be of type timestamp or unix timestamp, got: %T %v", columnValue, columnValue) - } - - if metricIndex >= 0 { - if columnValue, ok := values[metricIndex].(string); ok { - metric = columnValue - } else { - return fmt.Errorf("Column metric must be of type CHAR, VARCHAR, NCHAR or NVARCHAR. metric column name: %s type: %s but datatype is %T", columnNames[metricIndex], columnTypes[metricIndex].DatabaseTypeName(), values[metricIndex]) - } - } - - for i, col := range columnNames { - if i == timeIndex || i == metricIndex { - continue - } - - if value, err = tsdb.ConvertSqlValueColumnToFloat(col, values[i]); err != nil { - return err - } - - if metricIndex == -1 { - metric = col - } - - series, exist := pointsBySeries[metric] - if !exist { - series = &tsdb.TimeSeries{Name: metric} - pointsBySeries[metric] = series - seriesByQueryOrder.PushBack(metric) - } - - if fillMissing { - var intervalStart float64 - if !exist { - intervalStart = float64(tsdbQuery.TimeRange.MustGetFrom().UnixNano() / 1e6) - } else { - intervalStart = series.Points[len(series.Points)-1][1].Float64 + fillInterval - } - - // align interval start - intervalStart = math.Floor(intervalStart/fillInterval) * fillInterval - - for i := intervalStart; i < timestamp; i += fillInterval { - series.Points = append(series.Points, tsdb.TimePoint{fillValue, null.FloatFrom(i)}) - rowCount++ - } - } - - series.Points = append(series.Points, tsdb.TimePoint{value, null.FloatFrom(timestamp)}) - - e.log.Debug("Rows", "metric", metric, "time", timestamp, "value", value) - } - } - - for elem := seriesByQueryOrder.Front(); elem != nil; elem = elem.Next() { - key := elem.Value.(string) - result.Series = append(result.Series, pointsBySeries[key]) - - if fillMissing { - series := pointsBySeries[key] - // fill in values from last fetched value till interval end - intervalStart := series.Points[len(series.Points)-1][1].Float64 - intervalEnd := float64(tsdbQuery.TimeRange.MustGetTo().UnixNano() / 1e6) - - // align interval start - intervalStart = math.Floor(intervalStart/fillInterval) * fillInterval - for i := intervalStart + fillInterval; i < intervalEnd; i += fillInterval { - series.Points = append(series.Points, tsdb.TimePoint{fillValue, null.FloatFrom(i)}) - rowCount++ - } - } - } - - result.Meta.Set("rowCount", rowCount) - return nil -} diff --git a/pkg/tsdb/mssql/mssql_test.go b/pkg/tsdb/mssql/mssql_test.go index db04d6d1f02..86484cb9d5e 100644 --- a/pkg/tsdb/mssql/mssql_test.go +++ b/pkg/tsdb/mssql/mssql_test.go @@ -8,8 +8,9 @@ import ( "time" "github.com/go-xorm/xorm" + "github.com/grafana/grafana/pkg/components/securejsondata" "github.com/grafana/grafana/pkg/components/simplejson" - "github.com/grafana/grafana/pkg/log" + "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/sqlstore/sqlutil" "github.com/grafana/grafana/pkg/tsdb" . "github.com/smartystreets/goconvey/convey" @@ -19,8 +20,9 @@ import ( // The tests require a MSSQL db named grafanatest and a user/password grafana/Password! // Use the docker/blocks/mssql_tests/docker-compose.yaml to spin up a // preconfigured MSSQL server suitable for running these tests. -// There is also a dashboard.json in same directory that you can import to Grafana -// once you've created a datasource for the test server/database. +// There is also a datasource and dashboard provisioned by devenv scripts that you can +// use to verify that the generated data are vizualized as expected, see +// devenv/README.md for setup instructions. // If needed, change the variable below to the IP address of the database. var serverIP = "localhost" @@ -28,19 +30,25 @@ func TestMSSQL(t *testing.T) { SkipConvey("MSSQL", t, func() { x := InitMSSQLTestDB(t) - endpoint := &MssqlQueryEndpoint{ - sqlEngine: &tsdb.DefaultSqlEngine{ - MacroEngine: NewMssqlMacroEngine(), - XormEngine: x, - }, - log: log.New("tsdb.mssql"), + origXormEngine := tsdb.NewXormEngine + tsdb.NewXormEngine = func(d, c string) (*xorm.Engine, error) { + return x, nil } - sess := x.NewSession() - defer sess.Close() + endpoint, err := newMssqlQueryEndpoint(&models.DataSource{ + JsonData: simplejson.New(), + SecureJsonData: securejsondata.SecureJsonData{}, + }) + So(err, ShouldBeNil) + sess := x.NewSession() fromStart := time.Date(2018, 3, 15, 13, 0, 0, 0, time.UTC).In(time.Local) + Reset(func() { + sess.Close() + tsdb.NewXormEngine = origXormEngine + }) + Convey("Given a table with different native data types", func() { sql := ` IF OBJECT_ID('dbo.[mssql_types]', 'U') IS NOT NULL From 318b8c5a2346d60ede4fe2f01ffb0f665501709c Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Thu, 26 Jul 2018 18:12:00 +0200 Subject: [PATCH 177/786] update devenv datasources and dashboards for sql datasources Removed dashboards from docker blocks --- devenv/datasources.yaml | 28 +++- .../datasource_tests_mssql_fakedata.json | 79 ++++------ .../datasource_tests_mssql_unittest.json | 142 ++++++++---------- .../datasource_tests_mysql_fakedata.json | 68 +++------ .../datasource_tests_mysql_unittest.json | 136 ++++++++--------- .../datasource_tests_postgres_fakedata.json | 88 +++++------ .../datasource_tests_postgres_unittest.json | 142 ++++++++---------- 7 files changed, 306 insertions(+), 377 deletions(-) rename docker/blocks/mssql/dashboard.json => devenv/dev-dashboards/datasource_tests_mssql_fakedata.json (92%) rename docker/blocks/mssql_tests/dashboard.json => devenv/dev-dashboards/datasource_tests_mssql_unittest.json (96%) rename docker/blocks/mysql/dashboard.json => devenv/dev-dashboards/datasource_tests_mysql_fakedata.json (92%) rename docker/blocks/mysql_tests/dashboard.json => devenv/dev-dashboards/datasource_tests_mysql_unittest.json (96%) rename docker/blocks/postgres/dashboard.json => devenv/dev-dashboards/datasource_tests_postgres_fakedata.json (91%) rename docker/blocks/postgres_tests/dashboard.json => devenv/dev-dashboards/datasource_tests_postgres_unittest.json (95%) diff --git a/devenv/datasources.yaml b/devenv/datasources.yaml index 241381097b1..a4e9bf05641 100644 --- a/devenv/datasources.yaml +++ b/devenv/datasources.yaml @@ -51,12 +51,28 @@ datasources: user: grafana password: password + - name: gdev-mysql-ds-tests + type: mysql + url: localhost:3306 + database: grafana_ds_tests + user: grafana + password: password + - name: gdev-mssql type: mssql url: localhost:1433 database: grafana user: grafana - password: "Password!" + secureJsonData: + password: Password! + + - name: gdev-mssql-ds-tests + type: mssql + url: localhost:1433 + database: grafanatest + user: grafana + secureJsonData: + password: Password! - name: gdev-postgres type: postgres @@ -68,6 +84,16 @@ datasources: jsonData: sslmode: "disable" + - name: gdev-postgres-ds-tests + type: postgres + url: localhost:5432 + database: grafanadstest + user: grafanatest + secureJsonData: + password: grafanatest + jsonData: + sslmode: "disable" + - name: gdev-cloudwatch type: cloudwatch editable: true diff --git a/docker/blocks/mssql/dashboard.json b/devenv/dev-dashboards/datasource_tests_mssql_fakedata.json similarity index 92% rename from docker/blocks/mssql/dashboard.json rename to devenv/dev-dashboards/datasource_tests_mssql_fakedata.json index ce9aa141a75..4350b5e44a8 100644 --- a/docker/blocks/mssql/dashboard.json +++ b/devenv/dev-dashboards/datasource_tests_mssql_fakedata.json @@ -1,40 +1,4 @@ { - "__inputs": [ - { - "name": "DS_MSSQL", - "label": "MSSQL", - "description": "", - "type": "datasource", - "pluginId": "mssql", - "pluginName": "MSSQL" - } - ], - "__requires": [ - { - "type": "grafana", - "id": "grafana", - "name": "Grafana", - "version": "5.0.0" - }, - { - "type": "panel", - "id": "graph", - "name": "Graph", - "version": "5.0.0" - }, - { - "type": "datasource", - "id": "mssql", - "name": "MSSQL", - "version": "1.0.0" - }, - { - "type": "panel", - "id": "table", - "name": "Table", - "version": "5.0.0" - } - ], "annotations": { "list": [ { @@ -52,8 +16,8 @@ "editable": true, "gnetId": null, "graphTooltip": 0, - "id": null, - "iteration": 1520976748896, + "id": 203, + "iteration": 1532618661457, "links": [], "panels": [ { @@ -63,7 +27,7 @@ "bars": false, "dashLength": 10, "dashes": false, - "datasource": "${DS_MSSQL}", + "datasource": "gdev-mssql", "fill": 2, "gridPos": { "h": 9, @@ -149,14 +113,18 @@ "min": null, "show": true } - ] + ], + "yaxis": { + "align": false, + "alignLevel": null + } }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, - "datasource": "${DS_MSSQL}", + "datasource": "gdev-mssql", "fill": 2, "gridPos": { "h": 18, @@ -234,14 +202,18 @@ "min": null, "show": true } - ] + ], + "yaxis": { + "align": false, + "alignLevel": null + } }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, - "datasource": "${DS_MSSQL}", + "datasource": "gdev-mssql", "fill": 2, "gridPos": { "h": 9, @@ -313,11 +285,15 @@ "min": null, "show": true } - ] + ], + "yaxis": { + "align": false, + "alignLevel": null + } }, { "columns": [], - "datasource": "${DS_MSSQL}", + "datasource": "gdev-mssql", "fontSize": "100%", "gridPos": { "h": 10, @@ -371,13 +347,13 @@ ], "schemaVersion": 16, "style": "dark", - "tags": [], + "tags": ["gdev", "mssql", "fake-data-gen"], "templating": { "list": [ { "allValue": null, "current": {}, - "datasource": "${DS_MSSQL}", + "datasource": "gdev-mssql", "hide": 0, "includeAll": false, "label": "Datacenter", @@ -387,6 +363,7 @@ "query": "SELECT DISTINCT datacenter FROM grafana_metric", "refresh": 1, "regex": "", + "skipUrlSync": false, "sort": 1, "tagValuesQuery": "", "tags": [], @@ -397,7 +374,7 @@ { "allValue": null, "current": {}, - "datasource": "${DS_MSSQL}", + "datasource": "gdev-mssql", "hide": 0, "includeAll": true, "label": "Hostname", @@ -407,6 +384,7 @@ "query": "SELECT DISTINCT hostname FROM grafana_metric WHERE datacenter='$datacenter'", "refresh": 1, "regex": "", + "skipUrlSync": false, "sort": 1, "tagValuesQuery": "", "tags": [], @@ -499,6 +477,7 @@ ], "query": "1s,10s,30s,1m,5m,10m,30m,1h,6h,12h,1d,7d,14d,30d", "refresh": 2, + "skipUrlSync": false, "type": "interval" } ] @@ -533,7 +512,7 @@ ] }, "timezone": "", - "title": "Grafana Fake Data Gen - MSSQL", + "title": "Datasource tests - MSSQL", "uid": "86Js1xRmk", - "version": 11 + "version": 1 } \ No newline at end of file diff --git a/docker/blocks/mssql_tests/dashboard.json b/devenv/dev-dashboards/datasource_tests_mssql_unittest.json similarity index 96% rename from docker/blocks/mssql_tests/dashboard.json rename to devenv/dev-dashboards/datasource_tests_mssql_unittest.json index 80994254093..5c8eb8243a3 100644 --- a/docker/blocks/mssql_tests/dashboard.json +++ b/devenv/dev-dashboards/datasource_tests_mssql_unittest.json @@ -1,40 +1,4 @@ { - "__inputs": [ - { - "name": "DS_MSSQL_TEST", - "label": "MSSQL Test", - "description": "", - "type": "datasource", - "pluginId": "mssql", - "pluginName": "Microsoft SQL Server" - } - ], - "__requires": [ - { - "type": "grafana", - "id": "grafana", - "name": "Grafana", - "version": "5.0.0" - }, - { - "type": "panel", - "id": "graph", - "name": "Graph", - "version": "5.0.0" - }, - { - "type": "datasource", - "id": "mssql", - "name": "Microsoft SQL Server", - "version": "1.0.0" - }, - { - "type": "panel", - "id": "table", - "name": "Table", - "version": "5.0.0" - } - ], "annotations": { "list": [ { @@ -47,7 +11,7 @@ "type": "dashboard" }, { - "datasource": "${DS_MSSQL_TEST}", + "datasource": "gdev-mssql-ds-tests", "enable": false, "hide": false, "iconColor": "#6ed0e0", @@ -59,7 +23,7 @@ "type": "tags" }, { - "datasource": "${DS_MSSQL_TEST}", + "datasource": "gdev-mssql-ds-tests", "enable": false, "hide": false, "iconColor": "rgba(255, 96, 96, 1)", @@ -71,7 +35,7 @@ "type": "tags" }, { - "datasource": "${DS_MSSQL_TEST}", + "datasource": "gdev-mssql-ds-tests", "enable": false, "hide": false, "iconColor": "#7eb26d", @@ -83,7 +47,7 @@ "type": "tags" }, { - "datasource": "${DS_MSSQL_TEST}", + "datasource": "gdev-mssql-ds-tests", "enable": false, "hide": false, "iconColor": "#1f78c1", @@ -96,16 +60,17 @@ } ] }, + "description": "Run the mssql unit tests to generate the data backing this dashboard", "editable": true, "gnetId": null, "graphTooltip": 0, - "id": null, - "iteration": 1523320861623, + "id": 35, + "iteration": 1532618879985, "links": [], "panels": [ { "columns": [], - "datasource": "${DS_MSSQL_TEST}", + "datasource": "gdev-mssql-ds-tests", "fontSize": "100%", "gridPos": { "h": 4, @@ -152,7 +117,7 @@ }, { "columns": [], - "datasource": "${DS_MSSQL_TEST}", + "datasource": "gdev-mssql-ds-tests", "fontSize": "100%", "gridPos": { "h": 3, @@ -206,7 +171,7 @@ }, { "columns": [], - "datasource": "${DS_MSSQL_TEST}", + "datasource": "gdev-mssql-ds-tests", "fontSize": "100%", "gridPos": { "h": 3, @@ -260,7 +225,7 @@ }, { "columns": [], - "datasource": "${DS_MSSQL_TEST}", + "datasource": "gdev-mssql-ds-tests", "fontSize": "100%", "gridPos": { "h": 3, @@ -314,7 +279,7 @@ }, { "columns": [], - "datasource": "${DS_MSSQL_TEST}", + "datasource": "gdev-mssql-ds-tests", "fontSize": "100%", "gridPos": { "h": 3, @@ -371,7 +336,7 @@ "bars": false, "dashLength": 10, "dashes": false, - "datasource": "${DS_MSSQL_TEST}", + "datasource": "gdev-mssql-ds-tests", "fill": 2, "gridPos": { "h": 9, @@ -454,7 +419,7 @@ "bars": false, "dashLength": 10, "dashes": false, - "datasource": "${DS_MSSQL_TEST}", + "datasource": "gdev-mssql-ds-tests", "fill": 2, "gridPos": { "h": 9, @@ -537,7 +502,7 @@ "bars": false, "dashLength": 10, "dashes": false, - "datasource": "${DS_MSSQL_TEST}", + "datasource": "gdev-mssql-ds-tests", "fill": 2, "gridPos": { "h": 9, @@ -620,7 +585,7 @@ "bars": true, "dashLength": 10, "dashes": false, - "datasource": "${DS_MSSQL_TEST}", + "datasource": "gdev-mssql-ds-tests", "fill": 2, "gridPos": { "h": 9, @@ -703,7 +668,7 @@ "bars": true, "dashLength": 10, "dashes": false, - "datasource": "${DS_MSSQL_TEST}", + "datasource": "gdev-mssql-ds-tests", "fill": 2, "gridPos": { "h": 9, @@ -786,7 +751,7 @@ "bars": true, "dashLength": 10, "dashes": false, - "datasource": "${DS_MSSQL_TEST}", + "datasource": "gdev-mssql-ds-tests", "fill": 2, "gridPos": { "h": 9, @@ -869,7 +834,7 @@ "bars": false, "dashLength": 10, "dashes": false, - "datasource": "${DS_MSSQL_TEST}", + "datasource": "gdev-mssql-ds-tests", "fill": 2, "gridPos": { "h": 8, @@ -962,7 +927,7 @@ "bars": false, "dashLength": 10, "dashes": false, - "datasource": "${DS_MSSQL_TEST}", + "datasource": "gdev-mssql-ds-tests", "fill": 2, "gridPos": { "h": 8, @@ -1065,7 +1030,7 @@ "bars": false, "dashLength": 10, "dashes": false, - "datasource": "${DS_MSSQL_TEST}", + "datasource": "gdev-mssql-ds-tests", "fill": 2, "gridPos": { "h": 8, @@ -1158,7 +1123,7 @@ "bars": false, "dashLength": 10, "dashes": false, - "datasource": "${DS_MSSQL_TEST}", + "datasource": "gdev-mssql-ds-tests", "fill": 2, "gridPos": { "h": 8, @@ -1243,7 +1208,7 @@ "bars": false, "dashLength": 10, "dashes": false, - "datasource": "${DS_MSSQL_TEST}", + "datasource": "gdev-mssql-ds-tests", "fill": 2, "gridPos": { "h": 8, @@ -1336,7 +1301,7 @@ "bars": false, "dashLength": 10, "dashes": false, - "datasource": "${DS_MSSQL_TEST}", + "datasource": "gdev-mssql-ds-tests", "fill": 2, "gridPos": { "h": 8, @@ -1421,7 +1386,7 @@ "bars": false, "dashLength": 10, "dashes": false, - "datasource": "${DS_MSSQL_TEST}", + "datasource": "gdev-mssql-ds-tests", "fill": 2, "gridPos": { "h": 8, @@ -1514,7 +1479,7 @@ "bars": false, "dashLength": 10, "dashes": false, - "datasource": "${DS_MSSQL_TEST}", + "datasource": "gdev-mssql-ds-tests", "fill": 2, "gridPos": { "h": 8, @@ -1599,7 +1564,7 @@ "bars": false, "dashLength": 10, "dashes": false, - "datasource": "${DS_MSSQL_TEST}", + "datasource": "gdev-mssql-ds-tests", "fill": 2, "gridPos": { "h": 8, @@ -1686,7 +1651,7 @@ "bars": false, "dashLength": 10, "dashes": false, - "datasource": "${DS_MSSQL_TEST}", + "datasource": "gdev-mssql-ds-tests", "fill": 2, "gridPos": { "h": 8, @@ -1773,7 +1738,7 @@ "bars": true, "dashLength": 10, "dashes": false, - "datasource": "${DS_MSSQL_TEST}", + "datasource": "gdev-mssql-ds-tests", "fill": 1, "gridPos": { "h": 8, @@ -1867,7 +1832,7 @@ "bars": true, "dashLength": 10, "dashes": false, - "datasource": "${DS_MSSQL_TEST}", + "datasource": "gdev-mssql-ds-tests", "fill": 1, "gridPos": { "h": 8, @@ -1954,7 +1919,7 @@ "bars": true, "dashLength": 10, "dashes": false, - "datasource": "${DS_MSSQL_TEST}", + "datasource": "gdev-mssql-ds-tests", "fill": 1, "gridPos": { "h": 8, @@ -2048,7 +2013,7 @@ "bars": true, "dashLength": 10, "dashes": false, - "datasource": "${DS_MSSQL_TEST}", + "datasource": "gdev-mssql-ds-tests", "fill": 1, "gridPos": { "h": 8, @@ -2135,7 +2100,7 @@ "bars": true, "dashLength": 10, "dashes": false, - "datasource": "${DS_MSSQL_TEST}", + "datasource": "gdev-mssql-ds-tests", "fill": 1, "gridPos": { "h": 8, @@ -2229,7 +2194,7 @@ "bars": true, "dashLength": 10, "dashes": false, - "datasource": "${DS_MSSQL_TEST}", + "datasource": "gdev-mssql-ds-tests", "fill": 1, "gridPos": { "h": 8, @@ -2316,7 +2281,7 @@ "bars": true, "dashLength": 10, "dashes": false, - "datasource": "${DS_MSSQL_TEST}", + "datasource": "gdev-mssql-ds-tests", "fill": 1, "gridPos": { "h": 8, @@ -2410,7 +2375,7 @@ "bars": true, "dashLength": 10, "dashes": false, - "datasource": "${DS_MSSQL_TEST}", + "datasource": "gdev-mssql-ds-tests", "fill": 1, "gridPos": { "h": 8, @@ -2496,22 +2461,44 @@ "refresh": false, "schemaVersion": 16, "style": "dark", - "tags": [], + "tags": ["gdev", "mssql"], "templating": { "list": [ { "allValue": "'ALL'", - "current": {}, - "datasource": "${DS_MSSQL_TEST}", + "current": { + "selected": true, + "tags": [], + "text": "All", + "value": "$__all" + }, + "datasource": "gdev-mssql-ds-tests", "hide": 0, "includeAll": true, "label": "Metric", "multi": false, "name": "metric", - "options": [], + "options": [ + { + "selected": true, + "text": "All", + "value": "$__all" + }, + { + "selected": false, + "text": "Metric A", + "value": "Metric A" + }, + { + "selected": false, + "text": "Metric B", + "value": "Metric B" + } + ], "query": "SELECT DISTINCT measurement FROM metric_values", - "refresh": 1, + "refresh": 0, "regex": "", + "skipUrlSync": false, "sort": 0, "tagValuesQuery": "", "tags": [], @@ -2564,6 +2551,7 @@ ], "query": "1s,10s,30s,1m,5m,10m", "refresh": 2, + "skipUrlSync": false, "type": "interval" } ] @@ -2598,7 +2586,7 @@ ] }, "timezone": "", - "title": "Microsoft SQL Server Data Source Test", + "title": "Datasource tests - MSSQL (unit test)", "uid": "GlAqcPgmz", "version": 58 } \ No newline at end of file diff --git a/docker/blocks/mysql/dashboard.json b/devenv/dev-dashboards/datasource_tests_mysql_fakedata.json similarity index 92% rename from docker/blocks/mysql/dashboard.json rename to devenv/dev-dashboards/datasource_tests_mysql_fakedata.json index dba7847cc72..cef8fd4783f 100644 --- a/docker/blocks/mysql/dashboard.json +++ b/devenv/dev-dashboards/datasource_tests_mysql_fakedata.json @@ -1,40 +1,4 @@ { - "__inputs": [ - { - "name": "DS_MYSQL", - "label": "MySQL", - "description": "", - "type": "datasource", - "pluginId": "mysql", - "pluginName": "MySQL" - } - ], - "__requires": [ - { - "type": "grafana", - "id": "grafana", - "name": "Grafana", - "version": "5.0.0" - }, - { - "type": "panel", - "id": "graph", - "name": "Graph", - "version": "5.0.0" - }, - { - "type": "datasource", - "id": "mysql", - "name": "MySQL", - "version": "5.0.0" - }, - { - "type": "panel", - "id": "table", - "name": "Table", - "version": "5.0.0" - } - ], "annotations": { "list": [ { @@ -52,8 +16,8 @@ "editable": true, "gnetId": null, "graphTooltip": 0, - "id": null, - "iteration": 1523372133566, + "id": 4, + "iteration": 1532620738041, "links": [], "panels": [ { @@ -63,7 +27,7 @@ "bars": false, "dashLength": 10, "dashes": false, - "datasource": "${DS_MYSQL}", + "datasource": "gdev-mysql", "fill": 2, "gridPos": { "h": 9, @@ -161,7 +125,7 @@ "bars": false, "dashLength": 10, "dashes": false, - "datasource": "${DS_MYSQL}", + "datasource": "gdev-mysql", "fill": 2, "gridPos": { "h": 18, @@ -251,7 +215,7 @@ "bars": false, "dashLength": 10, "dashes": false, - "datasource": "${DS_MYSQL}", + "datasource": "gdev-mysql", "fill": 2, "gridPos": { "h": 9, @@ -332,7 +296,7 @@ }, { "columns": [], - "datasource": "${DS_MYSQL}", + "datasource": "gdev-mysql", "fontSize": "100%", "gridPos": { "h": 9, @@ -390,6 +354,7 @@ "schemaVersion": 16, "style": "dark", "tags": [ + "gdev", "fake-data-gen", "mysql" ], @@ -397,8 +362,11 @@ "list": [ { "allValue": null, - "current": {}, - "datasource": "${DS_MYSQL}", + "current": { + "text": "America", + "value": "America" + }, + "datasource": "gdev-mysql", "hide": 0, "includeAll": false, "label": "Datacenter", @@ -408,6 +376,7 @@ "query": "SELECT DISTINCT datacenter FROM grafana_metric", "refresh": 1, "regex": "", + "skipUrlSync": false, "sort": 1, "tagValuesQuery": "", "tags": [], @@ -417,8 +386,11 @@ }, { "allValue": null, - "current": {}, - "datasource": "${DS_MYSQL}", + "current": { + "text": "All", + "value": "$__all" + }, + "datasource": "gdev-mysql", "hide": 0, "includeAll": true, "label": "Hostname", @@ -428,6 +400,7 @@ "query": "SELECT DISTINCT hostname FROM grafana_metric WHERE datacenter='$datacenter'", "refresh": 1, "regex": "", + "skipUrlSync": false, "sort": 1, "tagValuesQuery": "", "tags": [], @@ -520,6 +493,7 @@ ], "query": "1s,10s,30s,1m,5m,10m,30m,1h,6h,12h,1d,7d,14d,30d", "refresh": 2, + "skipUrlSync": false, "type": "interval" } ] @@ -554,7 +528,7 @@ ] }, "timezone": "", - "title": "Grafana Fake Data Gen - MySQL", + "title": "Datasource tests - MySQL", "uid": "DGsCac3kz", "version": 8 } \ No newline at end of file diff --git a/docker/blocks/mysql_tests/dashboard.json b/devenv/dev-dashboards/datasource_tests_mysql_unittest.json similarity index 96% rename from docker/blocks/mysql_tests/dashboard.json rename to devenv/dev-dashboards/datasource_tests_mysql_unittest.json index 53f313315bd..2c20969da12 100644 --- a/docker/blocks/mysql_tests/dashboard.json +++ b/devenv/dev-dashboards/datasource_tests_mysql_unittest.json @@ -1,40 +1,4 @@ { - "__inputs": [ - { - "name": "DS_MYSQL_TEST", - "label": "MySQL TEST", - "description": "", - "type": "datasource", - "pluginId": "mysql", - "pluginName": "MySQL" - } - ], - "__requires": [ - { - "type": "grafana", - "id": "grafana", - "name": "Grafana", - "version": "5.0.0" - }, - { - "type": "panel", - "id": "graph", - "name": "Graph", - "version": "5.0.0" - }, - { - "type": "datasource", - "id": "mysql", - "name": "MySQL", - "version": "5.0.0" - }, - { - "type": "panel", - "id": "table", - "name": "Table", - "version": "5.0.0" - } - ], "annotations": { "list": [ { @@ -47,7 +11,7 @@ "type": "dashboard" }, { - "datasource": "${DS_MYSQL_TEST}", + "datasource": "gdev-mysql-ds-tests", "enable": false, "hide": false, "iconColor": "#6ed0e0", @@ -59,7 +23,7 @@ "type": "tags" }, { - "datasource": "${DS_MYSQL_TEST}", + "datasource": "gdev-mysql-ds-tests", "enable": false, "hide": false, "iconColor": "rgba(255, 96, 96, 1)", @@ -71,7 +35,7 @@ "type": "tags" }, { - "datasource": "${DS_MYSQL_TEST}", + "datasource": "gdev-mysql-ds-tests", "enable": false, "hide": false, "iconColor": "#7eb26d", @@ -83,7 +47,7 @@ "type": "tags" }, { - "datasource": "${DS_MYSQL_TEST}", + "datasource": "gdev-mysql-ds-tests", "enable": false, "hide": false, "iconColor": "#1f78c1", @@ -96,16 +60,17 @@ } ] }, + "description": "Run the mysql unit tests to generate the data backing this dashboard", "editable": true, "gnetId": null, "graphTooltip": 0, - "id": null, - "iteration": 1523320712115, + "id": 39, + "iteration": 1532620354037, "links": [], "panels": [ { "columns": [], - "datasource": "${DS_MYSQL_TEST}", + "datasource": "gdev-mysql-ds-tests", "fontSize": "100%", "gridPos": { "h": 4, @@ -152,7 +117,7 @@ }, { "columns": [], - "datasource": "${DS_MYSQL_TEST}", + "datasource": "gdev-mysql-ds-tests", "fontSize": "100%", "gridPos": { "h": 3, @@ -206,7 +171,7 @@ }, { "columns": [], - "datasource": "${DS_MYSQL_TEST}", + "datasource": "gdev-mysql-ds-tests", "fontSize": "100%", "gridPos": { "h": 3, @@ -260,7 +225,7 @@ }, { "columns": [], - "datasource": "${DS_MYSQL_TEST}", + "datasource": "gdev-mysql-ds-tests", "fontSize": "100%", "gridPos": { "h": 3, @@ -314,7 +279,7 @@ }, { "columns": [], - "datasource": "${DS_MYSQL_TEST}", + "datasource": "gdev-mysql-ds-tests", "fontSize": "100%", "gridPos": { "h": 3, @@ -371,7 +336,7 @@ "bars": false, "dashLength": 10, "dashes": false, - "datasource": "${DS_MYSQL_TEST}", + "datasource": "gdev-mysql-ds-tests", "fill": 2, "gridPos": { "h": 9, @@ -454,7 +419,7 @@ "bars": false, "dashLength": 10, "dashes": false, - "datasource": "${DS_MYSQL_TEST}", + "datasource": "gdev-mysql-ds-tests", "fill": 2, "gridPos": { "h": 9, @@ -537,7 +502,7 @@ "bars": false, "dashLength": 10, "dashes": false, - "datasource": "${DS_MYSQL_TEST}", + "datasource": "gdev-mysql-ds-tests", "fill": 2, "gridPos": { "h": 9, @@ -620,7 +585,7 @@ "bars": true, "dashLength": 10, "dashes": false, - "datasource": "${DS_MYSQL_TEST}", + "datasource": "gdev-mysql-ds-tests", "fill": 2, "gridPos": { "h": 9, @@ -703,7 +668,7 @@ "bars": true, "dashLength": 10, "dashes": false, - "datasource": "${DS_MYSQL_TEST}", + "datasource": "gdev-mysql-ds-tests", "fill": 2, "gridPos": { "h": 9, @@ -786,7 +751,7 @@ "bars": true, "dashLength": 10, "dashes": false, - "datasource": "${DS_MYSQL_TEST}", + "datasource": "gdev-mysql-ds-tests", "fill": 2, "gridPos": { "h": 9, @@ -869,7 +834,7 @@ "bars": false, "dashLength": 10, "dashes": false, - "datasource": "${DS_MYSQL_TEST}", + "datasource": "gdev-mysql-ds-tests", "fill": 2, "gridPos": { "h": 8, @@ -962,7 +927,7 @@ "bars": false, "dashLength": 10, "dashes": false, - "datasource": "${DS_MYSQL_TEST}", + "datasource": "gdev-mysql-ds-tests", "fill": 2, "gridPos": { "h": 8, @@ -1059,7 +1024,7 @@ "bars": false, "dashLength": 10, "dashes": false, - "datasource": "${DS_MYSQL_TEST}", + "datasource": "gdev-mysql-ds-tests", "fill": 2, "gridPos": { "h": 8, @@ -1152,7 +1117,7 @@ "bars": false, "dashLength": 10, "dashes": false, - "datasource": "${DS_MYSQL_TEST}", + "datasource": "gdev-mysql-ds-tests", "fill": 2, "gridPos": { "h": 8, @@ -1237,7 +1202,7 @@ "bars": false, "dashLength": 10, "dashes": false, - "datasource": "${DS_MYSQL_TEST}", + "datasource": "gdev-mysql-ds-tests", "fill": 2, "gridPos": { "h": 8, @@ -1330,7 +1295,7 @@ "bars": false, "dashLength": 10, "dashes": false, - "datasource": "${DS_MYSQL_TEST}", + "datasource": "gdev-mysql-ds-tests", "fill": 2, "gridPos": { "h": 8, @@ -1415,7 +1380,7 @@ "bars": false, "dashLength": 10, "dashes": false, - "datasource": "${DS_MYSQL_TEST}", + "datasource": "gdev-mysql-ds-tests", "fill": 2, "gridPos": { "h": 8, @@ -1508,7 +1473,7 @@ "bars": false, "dashLength": 10, "dashes": false, - "datasource": "${DS_MYSQL_TEST}", + "datasource": "gdev-mysql-ds-tests", "fill": 2, "gridPos": { "h": 8, @@ -1593,7 +1558,7 @@ "bars": true, "dashLength": 10, "dashes": false, - "datasource": "${DS_MYSQL_TEST}", + "datasource": "gdev-mysql-ds-tests", "fill": 1, "gridPos": { "h": 8, @@ -1687,7 +1652,7 @@ "bars": true, "dashLength": 10, "dashes": false, - "datasource": "${DS_MYSQL_TEST}", + "datasource": "gdev-mysql-ds-tests", "fill": 1, "gridPos": { "h": 8, @@ -1774,7 +1739,7 @@ "bars": true, "dashLength": 10, "dashes": false, - "datasource": "${DS_MYSQL_TEST}", + "datasource": "gdev-mysql-ds-tests", "fill": 1, "gridPos": { "h": 8, @@ -1868,7 +1833,7 @@ "bars": true, "dashLength": 10, "dashes": false, - "datasource": "${DS_MYSQL_TEST}", + "datasource": "gdev-mysql-ds-tests", "fill": 1, "gridPos": { "h": 8, @@ -1955,7 +1920,7 @@ "bars": true, "dashLength": 10, "dashes": false, - "datasource": "${DS_MYSQL_TEST}", + "datasource": "gdev-mysql-ds-tests", "fill": 1, "gridPos": { "h": 8, @@ -2049,7 +2014,7 @@ "bars": true, "dashLength": 10, "dashes": false, - "datasource": "${DS_MYSQL_TEST}", + "datasource": "gdev-mysql-ds-tests", "fill": 1, "gridPos": { "h": 8, @@ -2136,7 +2101,7 @@ "bars": true, "dashLength": 10, "dashes": false, - "datasource": "${DS_MYSQL_TEST}", + "datasource": "gdev-mysql-ds-tests", "fill": 1, "gridPos": { "h": 8, @@ -2230,7 +2195,7 @@ "bars": true, "dashLength": 10, "dashes": false, - "datasource": "${DS_MYSQL_TEST}", + "datasource": "gdev-mysql-ds-tests", "fill": 1, "gridPos": { "h": 8, @@ -2316,22 +2281,42 @@ "refresh": false, "schemaVersion": 16, "style": "dark", - "tags": [], + "tags": ["gdev", "mysql"], "templating": { "list": [ { "allValue": "", - "current": {}, - "datasource": "${DS_MYSQL_TEST}", + "current": { + "text": "All", + "value": "$__all" + }, + "datasource": "gdev-mysql-ds-tests", "hide": 0, "includeAll": true, "label": "Metric", "multi": true, "name": "metric", - "options": [], + "options": [ + { + "selected": true, + "text": "All", + "value": "$__all" + }, + { + "selected": false, + "text": "Metric A", + "value": "Metric A" + }, + { + "selected": false, + "text": "Metric B", + "value": "Metric B" + } + ], "query": "SELECT DISTINCT measurement FROM metric_values", - "refresh": 1, + "refresh": 0, "regex": "", + "skipUrlSync": false, "sort": 0, "tagValuesQuery": "", "tags": [], @@ -2384,6 +2369,7 @@ ], "query": "1s,10s,30s,1m,5m,10m", "refresh": 2, + "skipUrlSync": false, "type": "interval" } ] @@ -2418,7 +2404,7 @@ ] }, "timezone": "", - "title": "MySQL Data Source Test", + "title": "Datasource tests - MySQL (unittest)", "uid": "Hmf8FDkmz", "version": 12 } \ No newline at end of file diff --git a/docker/blocks/postgres/dashboard.json b/devenv/dev-dashboards/datasource_tests_postgres_fakedata.json similarity index 91% rename from docker/blocks/postgres/dashboard.json rename to devenv/dev-dashboards/datasource_tests_postgres_fakedata.json index 77b0ceac624..1afa6e25df8 100644 --- a/docker/blocks/postgres/dashboard.json +++ b/devenv/dev-dashboards/datasource_tests_postgres_fakedata.json @@ -1,40 +1,4 @@ { - "__inputs": [ - { - "name": "DS_POSTGRESQL", - "label": "PostgreSQL", - "description": "", - "type": "datasource", - "pluginId": "postgres", - "pluginName": "PostgreSQL" - } - ], - "__requires": [ - { - "type": "grafana", - "id": "grafana", - "name": "Grafana", - "version": "5.0.0" - }, - { - "type": "panel", - "id": "graph", - "name": "Graph", - "version": "" - }, - { - "type": "datasource", - "id": "postgres", - "name": "PostgreSQL", - "version": "1.0.0" - }, - { - "type": "panel", - "id": "table", - "name": "Table", - "version": "" - } - ], "annotations": { "list": [ { @@ -52,8 +16,8 @@ "editable": true, "gnetId": null, "graphTooltip": 0, - "id": null, - "iteration": 1518601837383, + "id": 5, + "iteration": 1532620601931, "links": [], "panels": [ { @@ -63,7 +27,7 @@ "bars": false, "dashLength": 10, "dashes": false, - "datasource": "${DS_POSTGRESQL}", + "datasource": "gdev-postgres", "fill": 2, "gridPos": { "h": 9, @@ -150,14 +114,18 @@ "min": null, "show": true } - ] + ], + "yaxis": { + "align": false, + "alignLevel": null + } }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, - "datasource": "${DS_POSTGRESQL}", + "datasource": "gdev-postgres", "fill": 2, "gridPos": { "h": 18, @@ -236,14 +204,18 @@ "min": null, "show": true } - ] + ], + "yaxis": { + "align": false, + "alignLevel": null + } }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, - "datasource": "${DS_POSTGRESQL}", + "datasource": "gdev-postgres", "fill": 2, "gridPos": { "h": 9, @@ -316,11 +288,15 @@ "min": null, "show": true } - ] + ], + "yaxis": { + "align": false, + "alignLevel": null + } }, { "columns": [], - "datasource": "${DS_POSTGRESQL}", + "datasource": "gdev-postgres", "fontSize": "100%", "gridPos": { "h": 9, @@ -377,6 +353,7 @@ "schemaVersion": 16, "style": "dark", "tags": [ + "gdev", "fake-data-gen", "postgres" ], @@ -384,8 +361,11 @@ "list": [ { "allValue": null, - "current": {}, - "datasource": "${DS_POSTGRESQL}", + "current": { + "text": "America", + "value": "America" + }, + "datasource": "gdev-postgres", "hide": 0, "includeAll": false, "label": "Datacenter", @@ -395,6 +375,7 @@ "query": "SELECT DISTINCT datacenter FROM grafana_metric", "refresh": 1, "regex": "", + "skipUrlSync": false, "sort": 1, "tagValuesQuery": "", "tags": [], @@ -404,8 +385,11 @@ }, { "allValue": null, - "current": {}, - "datasource": "${DS_POSTGRESQL}", + "current": { + "text": "All", + "value": "$__all" + }, + "datasource": "gdev-postgres", "hide": 0, "includeAll": true, "label": "Hostname", @@ -415,6 +399,7 @@ "query": "SELECT DISTINCT hostname FROM grafana_metric WHERE datacenter='$datacenter'", "refresh": 1, "regex": "", + "skipUrlSync": false, "sort": 1, "tagValuesQuery": "", "tags": [], @@ -507,6 +492,7 @@ ], "query": "1s,10s,30s,1m,5m,10m,30m,1h,6h,12h,1d,7d,14d,30d", "refresh": 2, + "skipUrlSync": false, "type": "interval" } ] @@ -541,7 +527,7 @@ ] }, "timezone": "", - "title": "Grafana Fake Data Gen - PostgreSQL", + "title": "Datasource tests - Postgres", "uid": "JYola5qzz", - "version": 1 + "version": 4 } \ No newline at end of file diff --git a/docker/blocks/postgres_tests/dashboard.json b/devenv/dev-dashboards/datasource_tests_postgres_unittest.json similarity index 95% rename from docker/blocks/postgres_tests/dashboard.json rename to devenv/dev-dashboards/datasource_tests_postgres_unittest.json index 9efbe90bdfe..d7d5f238e85 100644 --- a/docker/blocks/postgres_tests/dashboard.json +++ b/devenv/dev-dashboards/datasource_tests_postgres_unittest.json @@ -1,40 +1,4 @@ { - "__inputs": [ - { - "name": "DS_POSTGRES_TEST", - "label": "Postgres TEST", - "description": "", - "type": "datasource", - "pluginId": "postgres", - "pluginName": "PostgreSQL" - } - ], - "__requires": [ - { - "type": "grafana", - "id": "grafana", - "name": "Grafana", - "version": "5.0.0" - }, - { - "type": "panel", - "id": "graph", - "name": "Graph", - "version": "5.0.0" - }, - { - "type": "datasource", - "id": "postgres", - "name": "PostgreSQL", - "version": "5.0.0" - }, - { - "type": "panel", - "id": "table", - "name": "Table", - "version": "5.0.0" - } - ], "annotations": { "list": [ { @@ -47,7 +11,7 @@ "type": "dashboard" }, { - "datasource": "${DS_POSTGRES_TEST}", + "datasource": "gdev-postgres-ds-tests", "enable": false, "hide": false, "iconColor": "#6ed0e0", @@ -59,7 +23,7 @@ "type": "tags" }, { - "datasource": "${DS_POSTGRES_TEST}", + "datasource": "gdev-postgres-ds-tests", "enable": false, "hide": false, "iconColor": "rgba(255, 96, 96, 1)", @@ -71,7 +35,7 @@ "type": "tags" }, { - "datasource": "${DS_POSTGRES_TEST}", + "datasource": "gdev-postgres-ds-tests", "enable": false, "hide": false, "iconColor": "#7eb26d", @@ -83,7 +47,7 @@ "type": "tags" }, { - "datasource": "${DS_POSTGRES_TEST}", + "datasource": "gdev-postgres-ds-tests", "enable": false, "hide": false, "iconColor": "#1f78c1", @@ -96,16 +60,17 @@ } ] }, + "description": "Run the postgres unit tests to generate the data backing this dashboard", "editable": true, "gnetId": null, "graphTooltip": 0, - "id": null, - "iteration": 1523320929325, + "id": 38, + "iteration": 1532619575136, "links": [], "panels": [ { "columns": [], - "datasource": "${DS_POSTGRES_TEST}", + "datasource": "gdev-postgres-ds-tests", "fontSize": "100%", "gridPos": { "h": 4, @@ -152,7 +117,7 @@ }, { "columns": [], - "datasource": "${DS_POSTGRES_TEST}", + "datasource": "gdev-postgres-ds-tests", "fontSize": "100%", "gridPos": { "h": 3, @@ -206,7 +171,7 @@ }, { "columns": [], - "datasource": "${DS_POSTGRES_TEST}", + "datasource": "gdev-postgres-ds-tests", "fontSize": "100%", "gridPos": { "h": 3, @@ -260,7 +225,7 @@ }, { "columns": [], - "datasource": "${DS_POSTGRES_TEST}", + "datasource": "gdev-postgres-ds-tests", "fontSize": "100%", "gridPos": { "h": 3, @@ -314,7 +279,7 @@ }, { "columns": [], - "datasource": "${DS_POSTGRES_TEST}", + "datasource": "gdev-postgres-ds-tests", "fontSize": "100%", "gridPos": { "h": 3, @@ -371,7 +336,7 @@ "bars": false, "dashLength": 10, "dashes": false, - "datasource": "${DS_POSTGRES_TEST}", + "datasource": "gdev-postgres-ds-tests", "fill": 2, "gridPos": { "h": 9, @@ -454,7 +419,7 @@ "bars": false, "dashLength": 10, "dashes": false, - "datasource": "${DS_POSTGRES_TEST}", + "datasource": "gdev-postgres-ds-tests", "fill": 2, "gridPos": { "h": 9, @@ -537,7 +502,7 @@ "bars": false, "dashLength": 10, "dashes": false, - "datasource": "${DS_POSTGRES_TEST}", + "datasource": "gdev-postgres-ds-tests", "fill": 2, "gridPos": { "h": 9, @@ -620,7 +585,7 @@ "bars": true, "dashLength": 10, "dashes": false, - "datasource": "${DS_POSTGRES_TEST}", + "datasource": "gdev-postgres-ds-tests", "fill": 2, "gridPos": { "h": 9, @@ -703,7 +668,7 @@ "bars": true, "dashLength": 10, "dashes": false, - "datasource": "${DS_POSTGRES_TEST}", + "datasource": "gdev-postgres-ds-tests", "fill": 2, "gridPos": { "h": 9, @@ -786,7 +751,7 @@ "bars": true, "dashLength": 10, "dashes": false, - "datasource": "${DS_POSTGRES_TEST}", + "datasource": "gdev-postgres-ds-tests", "fill": 2, "gridPos": { "h": 9, @@ -869,7 +834,7 @@ "bars": false, "dashLength": 10, "dashes": false, - "datasource": "${DS_POSTGRES_TEST}", + "datasource": "gdev-postgres-ds-tests", "fill": 2, "gridPos": { "h": 8, @@ -962,7 +927,7 @@ "bars": false, "dashLength": 10, "dashes": false, - "datasource": "${DS_POSTGRES_TEST}", + "datasource": "gdev-postgres-ds-tests", "fill": 2, "gridPos": { "h": 8, @@ -1047,7 +1012,7 @@ "bars": false, "dashLength": 10, "dashes": false, - "datasource": "${DS_POSTGRES_TEST}", + "datasource": "gdev-postgres-ds-tests", "fill": 2, "gridPos": { "h": 8, @@ -1140,7 +1105,7 @@ "bars": false, "dashLength": 10, "dashes": false, - "datasource": "${DS_POSTGRES_TEST}", + "datasource": "gdev-postgres-ds-tests", "fill": 2, "gridPos": { "h": 8, @@ -1225,7 +1190,7 @@ "bars": false, "dashLength": 10, "dashes": false, - "datasource": "${DS_POSTGRES_TEST}", + "datasource": "gdev-postgres-ds-tests", "fill": 2, "gridPos": { "h": 8, @@ -1318,7 +1283,7 @@ "bars": false, "dashLength": 10, "dashes": false, - "datasource": "${DS_POSTGRES_TEST}", + "datasource": "gdev-postgres-ds-tests", "fill": 2, "gridPos": { "h": 8, @@ -1403,7 +1368,7 @@ "bars": false, "dashLength": 10, "dashes": false, - "datasource": "${DS_POSTGRES_TEST}", + "datasource": "gdev-postgres-ds-tests", "fill": 2, "gridPos": { "h": 8, @@ -1496,7 +1461,7 @@ "bars": false, "dashLength": 10, "dashes": false, - "datasource": "${DS_POSTGRES_TEST}", + "datasource": "gdev-postgres-ds-tests", "fill": 2, "gridPos": { "h": 8, @@ -1581,7 +1546,7 @@ "bars": true, "dashLength": 10, "dashes": false, - "datasource": "${DS_POSTGRES_TEST}", + "datasource": "gdev-postgres-ds-tests", "fill": 1, "gridPos": { "h": 8, @@ -1675,7 +1640,7 @@ "bars": true, "dashLength": 10, "dashes": false, - "datasource": "${DS_POSTGRES_TEST}", + "datasource": "gdev-postgres-ds-tests", "fill": 1, "gridPos": { "h": 8, @@ -1762,7 +1727,7 @@ "bars": true, "dashLength": 10, "dashes": false, - "datasource": "${DS_POSTGRES_TEST}", + "datasource": "gdev-postgres-ds-tests", "fill": 1, "gridPos": { "h": 8, @@ -1856,7 +1821,7 @@ "bars": true, "dashLength": 10, "dashes": false, - "datasource": "${DS_POSTGRES_TEST}", + "datasource": "gdev-postgres-ds-tests", "fill": 1, "gridPos": { "h": 8, @@ -1943,7 +1908,7 @@ "bars": true, "dashLength": 10, "dashes": false, - "datasource": "${DS_POSTGRES_TEST}", + "datasource": "gdev-postgres-ds-tests", "fill": 1, "gridPos": { "h": 8, @@ -2037,7 +2002,7 @@ "bars": true, "dashLength": 10, "dashes": false, - "datasource": "${DS_POSTGRES_TEST}", + "datasource": "gdev-postgres-ds-tests", "fill": 1, "gridPos": { "h": 8, @@ -2124,7 +2089,7 @@ "bars": true, "dashLength": 10, "dashes": false, - "datasource": "${DS_POSTGRES_TEST}", + "datasource": "gdev-postgres-ds-tests", "fill": 1, "gridPos": { "h": 8, @@ -2218,7 +2183,7 @@ "bars": true, "dashLength": 10, "dashes": false, - "datasource": "${DS_POSTGRES_TEST}", + "datasource": "gdev-postgres-ds-tests", "fill": 1, "gridPos": { "h": 8, @@ -2304,22 +2269,46 @@ "refresh": false, "schemaVersion": 16, "style": "dark", - "tags": [], + "tags": ["gdev", "postgres"], "templating": { "list": [ { "allValue": null, - "current": {}, - "datasource": "${DS_POSTGRES_TEST}", + "current": { + "selected": true, + "tags": [], + "text": "All", + "value": [ + "$__all" + ] + }, + "datasource": "gdev-postgres-ds-tests", "hide": 0, "includeAll": true, "label": "Metric", "multi": true, "name": "metric", - "options": [], + "options": [ + { + "selected": true, + "text": "All", + "value": "$__all" + }, + { + "selected": false, + "text": "Metric A", + "value": "Metric A" + }, + { + "selected": false, + "text": "Metric B", + "value": "Metric B" + } + ], "query": "SELECT DISTINCT measurement FROM metric_values", - "refresh": 1, + "refresh": 0, "regex": "", + "skipUrlSync": false, "sort": 1, "tagValuesQuery": "", "tags": [], @@ -2372,6 +2361,7 @@ ], "query": "1s,10s,30s,1m,5m,10m", "refresh": 2, + "skipUrlSync": false, "type": "interval" } ] @@ -2406,7 +2396,7 @@ ] }, "timezone": "", - "title": "Postgres Data Source Test", + "title": "Datasource tests - Postgres (unittest)", "uid": "vHQdlVziz", - "version": 14 + "version": 17 } \ No newline at end of file From ab8fa0de7443136afeab82fcf8713fddbdc23a48 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Thu, 26 Jul 2018 21:39:02 +0200 Subject: [PATCH 178/786] elasticsearch: support reversed index patterns Now both [index-]pattern and pattern[-index] are supported --- .../elasticsearch/client/index_pattern.go | 35 ++++++++++++++----- .../client/index_pattern_test.go | 27 +++++++++++++- 2 files changed, 53 insertions(+), 9 deletions(-) diff --git a/pkg/tsdb/elasticsearch/client/index_pattern.go b/pkg/tsdb/elasticsearch/client/index_pattern.go index 8391e902ea4..952b5c4f806 100644 --- a/pkg/tsdb/elasticsearch/client/index_pattern.go +++ b/pkg/tsdb/elasticsearch/client/index_pattern.go @@ -248,13 +248,28 @@ var datePatternReplacements = map[string]string{ func formatDate(t time.Time, pattern string) string { var datePattern string - parts := strings.Split(strings.TrimLeft(pattern, "["), "]") - base := parts[0] - if len(parts) == 2 { - datePattern = parts[1] - } else { - datePattern = base - base = "" + base := "" + ltr := false + + if strings.HasPrefix(pattern, "[") { + parts := strings.Split(strings.TrimLeft(pattern, "["), "]") + base = parts[0] + if len(parts) == 2 { + datePattern = parts[1] + } else { + datePattern = base + base = "" + } + ltr = true + } else if strings.HasSuffix(pattern, "]") { + parts := strings.Split(strings.TrimRight(pattern, "]"), "[") + datePattern = parts[0] + if len(parts) == 2 { + base = parts[1] + } else { + base = "" + } + ltr = false } formatted := t.Format(patternToLayout(datePattern)) @@ -293,7 +308,11 @@ func formatDate(t time.Time, pattern string) string { formatted = strings.Replace(formatted, "", fmt.Sprintf("%d", t.Hour()), -1) } - return base + formatted + if ltr { + return base + formatted + } + + return formatted + base } func patternToLayout(pattern string) string { diff --git a/pkg/tsdb/elasticsearch/client/index_pattern_test.go b/pkg/tsdb/elasticsearch/client/index_pattern_test.go index 3bd823d8c87..ca20b39d532 100644 --- a/pkg/tsdb/elasticsearch/client/index_pattern_test.go +++ b/pkg/tsdb/elasticsearch/client/index_pattern_test.go @@ -28,29 +28,54 @@ func TestIndexPattern(t *testing.T) { to := fmt.Sprintf("%d", time.Date(2018, 5, 15, 17, 55, 0, 0, time.UTC).UnixNano()/int64(time.Millisecond)) indexPatternScenario(intervalHourly, "[data-]YYYY.MM.DD.HH", tsdb.NewTimeRange(from, to), func(indices []string) { - //So(indices, ShouldHaveLength, 1) + So(indices, ShouldHaveLength, 1) So(indices[0], ShouldEqual, "data-2018.05.15.17") }) + indexPatternScenario(intervalHourly, "YYYY.MM.DD.HH[-data]", tsdb.NewTimeRange(from, to), func(indices []string) { + So(indices, ShouldHaveLength, 1) + So(indices[0], ShouldEqual, "2018.05.15.17-data") + }) + indexPatternScenario(intervalDaily, "[data-]YYYY.MM.DD", tsdb.NewTimeRange(from, to), func(indices []string) { So(indices, ShouldHaveLength, 1) So(indices[0], ShouldEqual, "data-2018.05.15") }) + indexPatternScenario(intervalDaily, "YYYY.MM.DD[-data]", tsdb.NewTimeRange(from, to), func(indices []string) { + So(indices, ShouldHaveLength, 1) + So(indices[0], ShouldEqual, "2018.05.15-data") + }) + indexPatternScenario(intervalWeekly, "[data-]GGGG.WW", tsdb.NewTimeRange(from, to), func(indices []string) { So(indices, ShouldHaveLength, 1) So(indices[0], ShouldEqual, "data-2018.20") }) + indexPatternScenario(intervalWeekly, "GGGG.WW[-data]", tsdb.NewTimeRange(from, to), func(indices []string) { + So(indices, ShouldHaveLength, 1) + So(indices[0], ShouldEqual, "2018.20-data") + }) + indexPatternScenario(intervalMonthly, "[data-]YYYY.MM", tsdb.NewTimeRange(from, to), func(indices []string) { So(indices, ShouldHaveLength, 1) So(indices[0], ShouldEqual, "data-2018.05") }) + indexPatternScenario(intervalMonthly, "YYYY.MM[-data]", tsdb.NewTimeRange(from, to), func(indices []string) { + So(indices, ShouldHaveLength, 1) + So(indices[0], ShouldEqual, "2018.05-data") + }) + indexPatternScenario(intervalYearly, "[data-]YYYY", tsdb.NewTimeRange(from, to), func(indices []string) { So(indices, ShouldHaveLength, 1) So(indices[0], ShouldEqual, "data-2018") }) + + indexPatternScenario(intervalYearly, "YYYY[-data]", tsdb.NewTimeRange(from, to), func(indices []string) { + So(indices, ShouldHaveLength, 1) + So(indices[0], ShouldEqual, "2018-data") + }) }) Convey("Hourly interval", t, func() { From 48e5e65c73eea000bf2b702b8743de0146e29f86 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Fri, 27 Jul 2018 10:33:06 +0200 Subject: [PATCH 179/786] changelog: add notes about closing #12731 [skip ci] --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6409f094f65..ad1b63234e9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,7 @@ * **Units**: Polish złoty currency [#12691](https://github.com/grafana/grafana/pull/12691), thx [@mwegrzynek](https://github.com/mwegrzynek) * **Cloudwatch**: Improved error handling [#12489](https://github.com/grafana/grafana/issues/12489), thx [@mtanda](https://github.com/mtanda) * **Table**: Adjust header contrast for the light theme [#12668](https://github.com/grafana/grafana/issues/12668) +* **Elasticsearch**: For alerting/backend, support having index name to the right of pattern in index pattern [#12731](https://github.com/grafana/grafana/issues/12731) # 5.2.2 (2018-07-25) From 97f24733f5c2b3f1654663a11a8708f12f994820 Mon Sep 17 00:00:00 2001 From: Sven Klemm Date: Fri, 27 Jul 2018 10:58:08 +0200 Subject: [PATCH 180/786] remove tableschema from query builder ui --- .../plugins/datasource/postgres/meta_query.ts | 51 ++++++++++++------- .../postgres/partials/query.editor.html | 1 - .../datasource/postgres/postgres_query.ts | 11 ++-- .../plugins/datasource/postgres/query_ctrl.ts | 15 ------ .../postgres/specs/postgres_query.jest.ts | 36 +++++++++++-- 5 files changed, 73 insertions(+), 41 deletions(-) diff --git a/public/app/plugins/datasource/postgres/meta_query.ts b/public/app/plugins/datasource/postgres/meta_query.ts index 66ff8867393..64271c022cc 100644 --- a/public/app/plugins/datasource/postgres/meta_query.ts +++ b/public/app/plugins/datasource/postgres/meta_query.ts @@ -10,7 +10,6 @@ export class PostgresMetaQuery { // query that returns first table found that has a timestamptz column and a float column let query = ` SELECT - table_schema, table_name, ( SELECT column_name @@ -32,7 +31,10 @@ SELECT ) AS value_column FROM information_schema.tables t WHERE - table_schema !~* '^_|^pg_|information_schema' AND + table_schema IN ( + SELECT CASE WHEN trim(unnest) = \'"$user"\' THEN user ELSE trim(unnest) END + FROM unnest(string_to_array(current_setting(\'search_path\'),\',\')) + ) AND EXISTS ( SELECT 1 FROM information_schema.columns c @@ -53,23 +55,30 @@ LIMIT 1 return query; } - buildSchemaQuery() { - let query = 'SELECT quote_ident(schema_name) FROM information_schema.schemata WHERE'; - query += " schema_name !~* '^pg_|^_|information_schema' ORDER BY schema_name"; - - return query; - } - buildTableQuery() { - let query = 'SELECT quote_ident(table_name) FROM information_schema.tables WHERE '; - query += 'table_schema = ' + this.quoteIdentAsLiteral(this.target.schema); - query += ' ORDER BY table_name'; + let query = ` +SELECT quote_ident(table_name) +FROM information_schema.tables +WHERE + table_schema IN ( + SELECT CASE WHEN trim(unnest) = \'"$user"\' THEN user ELSE trim(unnest) END + FROM unnest(string_to_array(current_setting(\'search_path\'),\',\')) + ) +ORDER BY table_name`; return query; } buildColumnQuery(type?: string) { - let query = 'SELECT quote_ident(column_name) FROM information_schema.columns WHERE '; - query += 'table_schema = ' + this.quoteIdentAsLiteral(this.target.schema); + let query = ` +SELECT quote_ident(column_name) +FROM information_schema.columns +WHERE + table_schema IN ( + SELECT CASE WHEN trim(unnest) = \'"$user"\' THEN user ELSE trim(unnest) END + FROM unnest(string_to_array(current_setting(\'search_path\'),\',\')) + LIMIT 1 + ) +`; query += ' AND table_name = ' + this.quoteIdentAsLiteral(this.target.table); switch (type) { @@ -100,15 +109,23 @@ LIMIT 1 buildValueQuery(column: string) { let query = 'SELECT DISTINCT quote_literal(' + column + ')'; - query += ' FROM ' + this.target.schema + '.' + this.target.table; + query += ' FROM ' + this.target.table; query += ' WHERE $__timeFilter(' + this.target.timeColumn + ')'; query += ' ORDER BY 1 LIMIT 100'; return query; } buildDatatypeQuery(column: string) { - let query = 'SELECT data_type FROM information_schema.columns WHERE '; - query += ' table_schema = ' + this.quoteIdentAsLiteral(this.target.schema); + let query = ` +SELECT data_type +FROM information_schema.columns +WHERE + table_schema IN ( + SELECT CASE WHEN trim(unnest) = \'"$user"\' THEN user ELSE trim(unnest) END + FROM unnest(string_to_array(current_setting(\'search_path\'),\',\')) + LIMIT 1 + ) +`; query += ' AND table_name = ' + this.quoteIdentAsLiteral(this.target.table); query += ' AND column_name = ' + this.quoteIdentAsLiteral(column); return query; diff --git a/public/app/plugins/datasource/postgres/partials/query.editor.html b/public/app/plugins/datasource/postgres/partials/query.editor.html index 90dbdf2eee0..68711f3ea0b 100644 --- a/public/app/plugins/datasource/postgres/partials/query.editor.html +++ b/public/app/plugins/datasource/postgres/partials/query.editor.html @@ -13,7 +13,6 @@
    - diff --git a/public/app/plugins/datasource/postgres/postgres_query.ts b/public/app/plugins/datasource/postgres/postgres_query.ts index d0aa8a45841..3c1b1b681b4 100644 --- a/public/app/plugins/datasource/postgres/postgres_query.ts +++ b/public/app/plugins/datasource/postgres/postgres_query.ts @@ -11,7 +11,6 @@ export default class PostgresQuery { this.templateSrv = templateSrv; this.scopedVars = scopedVars; - target.schema = target.schema || 'public'; target.format = target.format || 'time_series'; target.timeColumn = target.timeColumn || 'time'; target.metricColumn = target.metricColumn || 'none'; @@ -147,13 +146,15 @@ export default class PostgresQuery { } if (special) { - let over = ''; + let overParts = []; if (this.hasMetricColumn()) { - over = 'PARTITION BY ' + this.target.metricColumn; + overParts.push('PARTITION BY ' + this.target.metricColumn); } if (!aggregate) { - over += ' ORDER BY ' + this.target.timeColumn; + overParts.push('ORDER BY ' + this.target.timeColumn); } + + let over = overParts.join(' '); switch (special.params[0]) { case 'increase': query = query + ' - lag(' + query + ') OVER (' + over + ')'; @@ -234,7 +235,7 @@ export default class PostgresQuery { } query += this.buildValueColumns(); - query += '\nFROM ' + this.target.schema + '.' + this.target.table; + query += '\nFROM ' + this.target.table; query += this.buildWhereClause(); query += this.buildGroupByClause(); diff --git a/public/app/plugins/datasource/postgres/query_ctrl.ts b/public/app/plugins/datasource/postgres/query_ctrl.ts index 68eac1cf34a..97db612e7db 100644 --- a/public/app/plugins/datasource/postgres/query_ctrl.ts +++ b/public/app/plugins/datasource/postgres/query_ctrl.ts @@ -28,7 +28,6 @@ export class PostgresQueryCtrl extends QueryCtrl { lastQueryMeta: QueryMeta; lastQueryError: string; showHelp: boolean; - schemaSegment: any; tableSegment: any; whereAdd: any; timeColumnSegment: any; @@ -59,8 +58,6 @@ export class PostgresQueryCtrl extends QueryCtrl { } } - this.schemaSegment = uiSegmentSrv.newSegment(this.target.schema); - if (!this.target.table) { this.tableSegment = uiSegmentSrv.newSegment({ value: 'select table', fake: true }); } else { @@ -119,18 +116,6 @@ export class PostgresQueryCtrl extends QueryCtrl { button.value = plusButton.value; } - getSchemaSegments() { - return this.datasource - .metricFindQuery(this.metaBuilder.buildSchemaQuery()) - .then(this.transformToSegments({})) - .catch(this.handleQueryError.bind(this)); - } - - schemaChanged() { - this.target.schema = this.schemaSegment.value; - this.panelCtrl.refresh(); - } - getTableSegments() { return this.datasource .metricFindQuery(this.metaBuilder.buildTableQuery()) diff --git a/public/app/plugins/datasource/postgres/specs/postgres_query.jest.ts b/public/app/plugins/datasource/postgres/specs/postgres_query.jest.ts index 659ca94496c..33d997d2d0a 100644 --- a/public/app/plugins/datasource/postgres/specs/postgres_query.jest.ts +++ b/public/app/plugins/datasource/postgres/specs/postgres_query.jest.ts @@ -66,6 +66,37 @@ describe('PostgresQuery', function() { expect(query.buildValueColumn(column)).toBe('v - lag(v) OVER (ORDER BY time) AS "a"'); }); + describe('When generating value column SQL with metric column', function() { + let query = new PostgresQuery({}, templateSrv); + query.target.metricColumn = 'host'; + + let column = [{ type: 'column', params: ['value'] }]; + expect(query.buildValueColumn(column)).toBe('value'); + column = [{ type: 'column', params: ['value'] }, { type: 'alias', params: ['alias'] }]; + expect(query.buildValueColumn(column)).toBe('value AS "alias"'); + column = [ + { type: 'column', params: ['v'] }, + { type: 'alias', params: ['a'] }, + { type: 'aggregate', params: ['max'] }, + ]; + expect(query.buildValueColumn(column)).toBe('max(v) AS "a"'); + column = [ + { type: 'column', params: ['v'] }, + { type: 'alias', params: ['a'] }, + { type: 'special', params: ['increase'] }, + ]; + expect(query.buildValueColumn(column)).toBe('v - lag(v) OVER (PARTITION BY host ORDER BY time) AS "a"'); + column = [ + { type: 'column', params: ['v'] }, + { type: 'alias', params: ['a'] }, + { type: 'aggregate', params: ['max'] }, + { type: 'special', params: ['increase'] }, + ]; + expect(query.buildValueColumn(column)).toBe( + 'max(v ORDER BY time) - lag(max(v ORDER BY time)) OVER (PARTITION BY host) AS "a"' + ); + }); + describe('When generating WHERE clause', function() { let query = new PostgresQuery({ where: [] }, templateSrv); @@ -95,18 +126,17 @@ describe('PostgresQuery', function() { describe('When generating complete statement', function() { let target = { timeColumn: 't', - schema: 'public', table: 'table', select: [[{ type: 'column', params: ['value'] }]], where: [], }; - let result = 'SELECT\n t AS "time",\n value\nFROM public.table\nORDER BY 1'; + let result = 'SELECT\n t AS "time",\n value\nFROM table\nORDER BY 1'; let query = new PostgresQuery(target, templateSrv); expect(query.buildQuery()).toBe(result); query.target.metricColumn = 'm'; - result = 'SELECT\n t AS "time",\n m AS metric,\n value\nFROM public.table\nORDER BY 1'; + result = 'SELECT\n t AS "time",\n m AS metric,\n value\nFROM table\nORDER BY 1'; expect(query.buildQuery()).toBe(result); }); }); From 675a031b6c9c367fe27de5e839c1d919ca09021d Mon Sep 17 00:00:00 2001 From: Tobias Skarhed Date: Fri, 27 Jul 2018 11:04:01 +0200 Subject: [PATCH 181/786] All except one passing --- public/app/plugins/panel/singlestat/module.ts | 5 ++++- public/app/plugins/panel/singlestat/specs/singlestat.jest.ts | 4 ++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/public/app/plugins/panel/singlestat/module.ts b/public/app/plugins/panel/singlestat/module.ts index 7fafb5902d1..b63182141c1 100644 --- a/public/app/plugins/panel/singlestat/module.ts +++ b/public/app/plugins/panel/singlestat/module.ts @@ -310,11 +310,14 @@ class SingleStatCtrl extends MetricsPanelCtrl { data.valueRounded = data.value; data.valueFormatted = formatFunc(data.value, this.dashboard.isTimezoneUtc()); } else { - console.log(lastPoint, lastValue); + // console.log(lastPoint, lastValue); + // console.log(this.panel.valueName); + // console.log(this.panel); data.value = this.series[0].stats[this.panel.valueName]; data.flotpairs = this.series[0].flotpairs; let decimalInfo = this.getDecimalsForValue(data.value); + console.log(decimalInfo); let formatFunc = kbn.valueFormats[this.panel.format]; data.valueFormatted = formatFunc(data.value, decimalInfo.decimals, decimalInfo.scaledDecimals); data.valueRounded = kbn.roundValue(data.value, decimalInfo.decimals); diff --git a/public/app/plugins/panel/singlestat/specs/singlestat.jest.ts b/public/app/plugins/panel/singlestat/specs/singlestat.jest.ts index 7b89f86250c..798298415a9 100644 --- a/public/app/plugins/panel/singlestat/specs/singlestat.jest.ts +++ b/public/app/plugins/panel/singlestat/specs/singlestat.jest.ts @@ -192,6 +192,8 @@ describe('SingleStatCtrl', function() { ) { ctx.setup(function() { ctx.data = [{ target: 'test.cpu1', datapoints: [[99.999, 1], [99.99999, 2]] }]; + ctx.ctrl.panel.valueName = 'avg'; + ctx.ctrl.panel.format = 'none'; }); it('Should be rounded', function() { @@ -259,7 +261,9 @@ describe('SingleStatCtrl', function() { singleStatScenario('with default values', function(ctx) { ctx.setup(function() { ctx.data = tableData; + ctx.ctrl.panel = {}; ctx.ctrl.panel.tableColumn = 'mean'; + ctx.ctrl.panel.format = 'none'; }); it('Should use first rows value as default main value', function() { From 47da3e3ae83f36207cedfa26e9b5d51ca21b112f Mon Sep 17 00:00:00 2001 From: Tobias Skarhed Date: Fri, 27 Jul 2018 11:28:16 +0200 Subject: [PATCH 182/786] All tests passing --- public/app/plugins/panel/singlestat/module.ts | 4 ---- public/app/plugins/panel/singlestat/specs/singlestat.jest.ts | 2 ++ 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/public/app/plugins/panel/singlestat/module.ts b/public/app/plugins/panel/singlestat/module.ts index b63182141c1..ebd2628b086 100644 --- a/public/app/plugins/panel/singlestat/module.ts +++ b/public/app/plugins/panel/singlestat/module.ts @@ -310,14 +310,10 @@ class SingleStatCtrl extends MetricsPanelCtrl { data.valueRounded = data.value; data.valueFormatted = formatFunc(data.value, this.dashboard.isTimezoneUtc()); } else { - // console.log(lastPoint, lastValue); - // console.log(this.panel.valueName); - // console.log(this.panel); data.value = this.series[0].stats[this.panel.valueName]; data.flotpairs = this.series[0].flotpairs; let decimalInfo = this.getDecimalsForValue(data.value); - console.log(decimalInfo); let formatFunc = kbn.valueFormats[this.panel.format]; data.valueFormatted = formatFunc(data.value, decimalInfo.decimals, decimalInfo.scaledDecimals); data.valueRounded = kbn.roundValue(data.value, decimalInfo.decimals); diff --git a/public/app/plugins/panel/singlestat/specs/singlestat.jest.ts b/public/app/plugins/panel/singlestat/specs/singlestat.jest.ts index 798298415a9..552ac2412d6 100644 --- a/public/app/plugins/panel/singlestat/specs/singlestat.jest.ts +++ b/public/app/plugins/panel/singlestat/specs/singlestat.jest.ts @@ -293,6 +293,7 @@ describe('SingleStatCtrl', function() { ctx.setup(function() { ctx.data = tableData; ctx.data[0].rows[0] = [1492759673649, 'ignore1', 99.99999, 'ignore2']; + ctx.ctrl.panel.mappingType = 0; ctx.ctrl.panel.tableColumn = 'mean'; }); @@ -310,6 +311,7 @@ describe('SingleStatCtrl', function() { ctx.setup(function() { ctx.data = tableData; ctx.data[0].rows[0] = [1492759673649, 'ignore1', 9.9, 'ignore2']; + ctx.ctrl.panel.mappingType = 2; ctx.ctrl.panel.tableColumn = 'mean'; ctx.ctrl.panel.valueMaps = [{ value: '10', text: 'OK' }]; }); From 3d21e42aac715c28fe3325bd3ce9f7a00cb39312 Mon Sep 17 00:00:00 2001 From: Tobias Skarhed Date: Fri, 27 Jul 2018 11:30:37 +0200 Subject: [PATCH 183/786] Remove Karma file --- .../singlestat/specs/singlestat_specs.ts | 362 ------------------ 1 file changed, 362 deletions(-) delete mode 100644 public/app/plugins/panel/singlestat/specs/singlestat_specs.ts diff --git a/public/app/plugins/panel/singlestat/specs/singlestat_specs.ts b/public/app/plugins/panel/singlestat/specs/singlestat_specs.ts deleted file mode 100644 index 217ec5ee04c..00000000000 --- a/public/app/plugins/panel/singlestat/specs/singlestat_specs.ts +++ /dev/null @@ -1,362 +0,0 @@ -import { describe, beforeEach, afterEach, it, sinon, expect, angularMocks } from 'test/lib/common'; - -import helpers from 'test/specs/helpers'; -import { SingleStatCtrl } from '../module'; -import moment from 'moment'; - -describe('SingleStatCtrl', function() { - var ctx = new helpers.ControllerTestContext(); - var epoch = 1505826363746; - var clock; - - function singleStatScenario(desc, func) { - describe(desc, function() { - ctx.setup = function(setupFunc) { - beforeEach(angularMocks.module('grafana.services')); - beforeEach(angularMocks.module('grafana.controllers')); - beforeEach( - angularMocks.module(function($compileProvider) { - $compileProvider.preAssignBindingsEnabled(true); - }) - ); - - beforeEach(ctx.providePhase()); - beforeEach(ctx.createPanelController(SingleStatCtrl)); - - beforeEach(function() { - setupFunc(); - ctx.ctrl.onDataReceived(ctx.data); - ctx.data = ctx.ctrl.data; - }); - }; - - func(ctx); - }); - } - - singleStatScenario('with defaults', function(ctx) { - ctx.setup(function() { - ctx.data = [{ target: 'test.cpu1', datapoints: [[10, 1], [20, 2]] }]; - }); - - it('Should use series avg as default main value', function() { - expect(ctx.data.value).to.be(15); - expect(ctx.data.valueRounded).to.be(15); - }); - - it('should set formatted falue', function() { - expect(ctx.data.valueFormatted).to.be('15'); - }); - }); - - singleStatScenario('showing serie name instead of value', function(ctx) { - ctx.setup(function() { - ctx.data = [{ target: 'test.cpu1', datapoints: [[10, 1], [20, 2]] }]; - ctx.ctrl.panel.valueName = 'name'; - }); - - it('Should use series avg as default main value', function() { - expect(ctx.data.value).to.be(0); - expect(ctx.data.valueRounded).to.be(0); - }); - - it('should set formatted value', function() { - expect(ctx.data.valueFormatted).to.be('test.cpu1'); - }); - }); - - singleStatScenario('showing last iso time instead of value', function(ctx) { - ctx.setup(function() { - ctx.data = [{ target: 'test.cpu1', datapoints: [[10, 12], [20, 1505634997920]] }]; - ctx.ctrl.panel.valueName = 'last_time'; - ctx.ctrl.panel.format = 'dateTimeAsIso'; - }); - - it('Should use time instead of value', function() { - expect(ctx.data.value).to.be(1505634997920); - expect(ctx.data.valueRounded).to.be(1505634997920); - }); - - it('should set formatted value', function() { - expect(ctx.data.valueFormatted).to.be(moment(1505634997920).format('YYYY-MM-DD HH:mm:ss')); - }); - }); - - singleStatScenario('showing last iso time instead of value (in UTC)', function(ctx) { - ctx.setup(function() { - ctx.data = [{ target: 'test.cpu1', datapoints: [[10, 12], [20, 1505634997920]] }]; - ctx.ctrl.panel.valueName = 'last_time'; - ctx.ctrl.panel.format = 'dateTimeAsIso'; - ctx.setIsUtc(true); - }); - - it('should set formatted value', function() { - expect(ctx.data.valueFormatted).to.be(moment.utc(1505634997920).format('YYYY-MM-DD HH:mm:ss')); - }); - }); - - singleStatScenario('showing last us time instead of value', function(ctx) { - ctx.setup(function() { - ctx.data = [{ target: 'test.cpu1', datapoints: [[10, 12], [20, 1505634997920]] }]; - ctx.ctrl.panel.valueName = 'last_time'; - ctx.ctrl.panel.format = 'dateTimeAsUS'; - }); - - it('Should use time instead of value', function() { - expect(ctx.data.value).to.be(1505634997920); - expect(ctx.data.valueRounded).to.be(1505634997920); - }); - - it('should set formatted value', function() { - expect(ctx.data.valueFormatted).to.be(moment(1505634997920).format('MM/DD/YYYY h:mm:ss a')); - }); - }); - - singleStatScenario('showing last us time instead of value (in UTC)', function(ctx) { - ctx.setup(function() { - ctx.data = [{ target: 'test.cpu1', datapoints: [[10, 12], [20, 1505634997920]] }]; - ctx.ctrl.panel.valueName = 'last_time'; - ctx.ctrl.panel.format = 'dateTimeAsUS'; - ctx.setIsUtc(true); - }); - - it('should set formatted value', function() { - expect(ctx.data.valueFormatted).to.be(moment.utc(1505634997920).format('MM/DD/YYYY h:mm:ss a')); - }); - }); - - singleStatScenario('showing last time from now instead of value', function(ctx) { - beforeEach(() => { - clock = sinon.useFakeTimers(epoch); - }); - - ctx.setup(function() { - ctx.data = [{ target: 'test.cpu1', datapoints: [[10, 12], [20, 1505634997920]] }]; - ctx.ctrl.panel.valueName = 'last_time'; - ctx.ctrl.panel.format = 'dateTimeFromNow'; - }); - - it('Should use time instead of value', function() { - expect(ctx.data.value).to.be(1505634997920); - expect(ctx.data.valueRounded).to.be(1505634997920); - }); - - it('should set formatted value', function() { - expect(ctx.data.valueFormatted).to.be('2 days ago'); - }); - - afterEach(() => { - clock.restore(); - }); - }); - - singleStatScenario('showing last time from now instead of value (in UTC)', function(ctx) { - beforeEach(() => { - clock = sinon.useFakeTimers(epoch); - }); - - ctx.setup(function() { - ctx.data = [{ target: 'test.cpu1', datapoints: [[10, 12], [20, 1505634997920]] }]; - ctx.ctrl.panel.valueName = 'last_time'; - ctx.ctrl.panel.format = 'dateTimeFromNow'; - ctx.setIsUtc(true); - }); - - it('should set formatted value', function() { - expect(ctx.data.valueFormatted).to.be('2 days ago'); - }); - - afterEach(() => { - clock.restore(); - }); - }); - - singleStatScenario('MainValue should use same number for decimals as displayed when checking thresholds', function( - ctx - ) { - ctx.setup(function() { - ctx.data = [{ target: 'test.cpu1', datapoints: [[99.999, 1], [99.99999, 2]] }]; - }); - - it('Should be rounded', function() { - expect(ctx.data.value).to.be(99.999495); - expect(ctx.data.valueRounded).to.be(100); - }); - - it('should set formatted value', function() { - expect(ctx.data.valueFormatted).to.be('100'); - }); - }); - - singleStatScenario('When value to text mapping is specified', function(ctx) { - ctx.setup(function() { - ctx.data = [{ target: 'test.cpu1', datapoints: [[9.9, 1]] }]; - ctx.ctrl.panel.valueMaps = [{ value: '10', text: 'OK' }]; - }); - - it('value should remain', function() { - expect(ctx.data.value).to.be(9.9); - }); - - it('round should be rounded up', function() { - expect(ctx.data.valueRounded).to.be(10); - }); - - it('Should replace value with text', function() { - expect(ctx.data.valueFormatted).to.be('OK'); - }); - }); - - singleStatScenario('When range to text mapping is specified for first range', function(ctx) { - ctx.setup(function() { - ctx.data = [{ target: 'test.cpu1', datapoints: [[41, 50]] }]; - ctx.ctrl.panel.mappingType = 2; - ctx.ctrl.panel.rangeMaps = [{ from: '10', to: '50', text: 'OK' }, { from: '51', to: '100', text: 'NOT OK' }]; - }); - - it('Should replace value with text OK', function() { - expect(ctx.data.valueFormatted).to.be('OK'); - }); - }); - - singleStatScenario('When range to text mapping is specified for other ranges', function(ctx) { - ctx.setup(function() { - ctx.data = [{ target: 'test.cpu1', datapoints: [[65, 75]] }]; - ctx.ctrl.panel.mappingType = 2; - ctx.ctrl.panel.rangeMaps = [{ from: '10', to: '50', text: 'OK' }, { from: '51', to: '100', text: 'NOT OK' }]; - }); - - it('Should replace value with text NOT OK', function() { - expect(ctx.data.valueFormatted).to.be('NOT OK'); - }); - }); - - describe('When table data', function() { - const tableData = [ - { - columns: [{ text: 'Time', type: 'time' }, { text: 'test1' }, { text: 'mean' }, { text: 'test2' }], - rows: [[1492759673649, 'ignore1', 15, 'ignore2']], - type: 'table', - }, - ]; - - singleStatScenario('with default values', function(ctx) { - ctx.setup(function() { - ctx.data = tableData; - ctx.ctrl.panel.tableColumn = 'mean'; - }); - - it('Should use first rows value as default main value', function() { - expect(ctx.data.value).to.be(15); - expect(ctx.data.valueRounded).to.be(15); - }); - - it('should set formatted value', function() { - expect(ctx.data.valueFormatted).to.be('15'); - }); - }); - - singleStatScenario('When table data has multiple columns', function(ctx) { - ctx.setup(function() { - ctx.data = tableData; - ctx.ctrl.panel.tableColumn = ''; - }); - - it('Should set column to first column that is not time', function() { - expect(ctx.ctrl.panel.tableColumn).to.be('test1'); - }); - }); - - singleStatScenario('MainValue should use same number for decimals as displayed when checking thresholds', function( - ctx - ) { - ctx.setup(function() { - ctx.data = tableData; - ctx.data[0].rows[0] = [1492759673649, 'ignore1', 99.99999, 'ignore2']; - ctx.ctrl.panel.tableColumn = 'mean'; - }); - - it('Should be rounded', function() { - expect(ctx.data.value).to.be(99.99999); - expect(ctx.data.valueRounded).to.be(100); - }); - - it('should set formatted falue', function() { - expect(ctx.data.valueFormatted).to.be('100'); - }); - }); - - singleStatScenario('When value to text mapping is specified', function(ctx) { - ctx.setup(function() { - ctx.data = tableData; - ctx.data[0].rows[0] = [1492759673649, 'ignore1', 9.9, 'ignore2']; - ctx.ctrl.panel.tableColumn = 'mean'; - ctx.ctrl.panel.valueMaps = [{ value: '10', text: 'OK' }]; - }); - - it('value should remain', function() { - expect(ctx.data.value).to.be(9.9); - }); - - it('round should be rounded up', function() { - expect(ctx.data.valueRounded).to.be(10); - }); - - it('Should replace value with text', function() { - expect(ctx.data.valueFormatted).to.be('OK'); - }); - }); - - singleStatScenario('When range to text mapping is specified for first range', function(ctx) { - ctx.setup(function() { - ctx.data = tableData; - ctx.data[0].rows[0] = [1492759673649, 'ignore1', 41, 'ignore2']; - ctx.ctrl.panel.tableColumn = 'mean'; - ctx.ctrl.panel.mappingType = 2; - ctx.ctrl.panel.rangeMaps = [{ from: '10', to: '50', text: 'OK' }, { from: '51', to: '100', text: 'NOT OK' }]; - }); - - it('Should replace value with text OK', function() { - expect(ctx.data.valueFormatted).to.be('OK'); - }); - }); - - singleStatScenario('When range to text mapping is specified for other ranges', function(ctx) { - ctx.setup(function() { - ctx.data = tableData; - ctx.data[0].rows[0] = [1492759673649, 'ignore1', 65, 'ignore2']; - ctx.ctrl.panel.tableColumn = 'mean'; - ctx.ctrl.panel.mappingType = 2; - ctx.ctrl.panel.rangeMaps = [{ from: '10', to: '50', text: 'OK' }, { from: '51', to: '100', text: 'NOT OK' }]; - }); - - it('Should replace value with text NOT OK', function() { - expect(ctx.data.valueFormatted).to.be('NOT OK'); - }); - }); - - singleStatScenario('When value is string', function(ctx) { - ctx.setup(function() { - ctx.data = tableData; - ctx.data[0].rows[0] = [1492759673649, 'ignore1', 65, 'ignore2']; - ctx.ctrl.panel.tableColumn = 'test1'; - }); - - it('Should replace value with text NOT OK', function() { - expect(ctx.data.valueFormatted).to.be('ignore1'); - }); - }); - - singleStatScenario('When value is zero', function(ctx) { - ctx.setup(function() { - ctx.data = tableData; - ctx.data[0].rows[0] = [1492759673649, 'ignore1', 0, 'ignore2']; - ctx.ctrl.panel.tableColumn = 'mean'; - }); - - it('Should return zero', function() { - expect(ctx.data.value).to.be(0); - }); - }); - }); -}); From bff7a293562125dc8423919f23a871d7141fa189 Mon Sep 17 00:00:00 2001 From: Tobias Skarhed Date: Fri, 27 Jul 2018 11:34:14 +0200 Subject: [PATCH 184/786] Cleanup --- .../panel/singlestat/specs/singlestat.jest.ts | 26 ------------------- 1 file changed, 26 deletions(-) diff --git a/public/app/plugins/panel/singlestat/specs/singlestat.jest.ts b/public/app/plugins/panel/singlestat/specs/singlestat.jest.ts index 552ac2412d6..7e8915ca537 100644 --- a/public/app/plugins/panel/singlestat/specs/singlestat.jest.ts +++ b/public/app/plugins/panel/singlestat/specs/singlestat.jest.ts @@ -1,6 +1,3 @@ -// import { describe, beforeEach, afterEach, it, sinon, expect, angularMocks } from 'test/lib/common'; - -// import helpers from 'test/specs/helpers'; import { SingleStatCtrl } from '../module'; import moment from 'moment'; @@ -30,17 +27,6 @@ describe('SingleStatCtrl', function() { function singleStatScenario(desc, func) { describe(desc, function() { ctx.setup = function(setupFunc) { - // beforeEach(angularMocks.module('grafana.services')); - // beforeEach(angularMocks.module('grafana.controllers')); - // beforeEach( - // angularMocks.module(function($compileProvider) { - // $compileProvider.preAssignBindingsEnabled(true); - // }) - // ); - - // beforeEach(ctx.providePhase()); - // beforeEach(ctx.createPanelController(SingleStatCtrl)); - beforeEach(function() { ctx.ctrl = new SingleStatCtrl($scope, $injector, {}); setupFunc(); @@ -107,7 +93,6 @@ describe('SingleStatCtrl', function() { ctx.data = [{ target: 'test.cpu1', datapoints: [[10, 12], [20, 5000]] }]; ctx.ctrl.panel.valueName = 'last_time'; ctx.ctrl.panel.format = 'dateTimeAsIso'; - // ctx.setIsUtc(true); ctx.ctrl.dashboard.isTimezoneUtc = () => true; }); @@ -139,7 +124,6 @@ describe('SingleStatCtrl', function() { ctx.data = [{ target: 'test.cpu1', datapoints: [[10, 12], [20, 5000]] }]; ctx.ctrl.panel.valueName = 'last_time'; ctx.ctrl.panel.format = 'dateTimeAsUS'; - // ctx.setIsUtc(true); ctx.ctrl.dashboard.isTimezoneUtc = () => true; }); @@ -149,11 +133,6 @@ describe('SingleStatCtrl', function() { }); singleStatScenario('showing last time from now instead of value', function(ctx) { - beforeEach(() => { - // clock = sinon.useFakeTimers(epoch); - //jest.useFakeTimers(); - }); - ctx.setup(function() { ctx.data = [{ target: 'test.cpu1', datapoints: [[10, 12], [20, 1505634997920]] }]; ctx.ctrl.panel.valueName = 'last_time'; @@ -168,10 +147,6 @@ describe('SingleStatCtrl', function() { it('should set formatted value', function() { expect(ctx.data.valueFormatted).toBe('2 days ago'); }); - - afterEach(() => { - // jest.clearAllTimers(); - }); }); singleStatScenario('showing last time from now instead of value (in UTC)', function(ctx) { @@ -179,7 +154,6 @@ describe('SingleStatCtrl', function() { ctx.data = [{ target: 'test.cpu1', datapoints: [[10, 12], [20, 1505634997920]] }]; ctx.ctrl.panel.valueName = 'last_time'; ctx.ctrl.panel.format = 'dateTimeFromNow'; - // ctx.setIsUtc(true); }); it('should set formatted value', function() { From e43feb7bfa0551125f82dbcf6503564227f091a1 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Fri, 27 Jul 2018 13:21:40 +0200 Subject: [PATCH 185/786] use const for rowlimit in sql engine --- pkg/tsdb/sql_engine.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/tsdb/sql_engine.go b/pkg/tsdb/sql_engine.go index 9321e8912dc..27ed37923a3 100644 --- a/pkg/tsdb/sql_engine.go +++ b/pkg/tsdb/sql_engine.go @@ -100,6 +100,8 @@ var NewSqlQueryEndpoint = func(config *SqlQueryEndpointConfiguration, rowTransfo return &queryEndpoint, nil } +const rowLimit = 1000000 + // Query is the main function for the SqlQueryEndpoint func (e *sqlQueryEndpoint) Query(ctx context.Context, dsInfo *models.DataSource, tsdbQuery *TsdbQuery) (*Response, error) { result := &Response{ @@ -164,7 +166,6 @@ func (e *sqlQueryEndpoint) transformToTable(query *Query, rows *core.Rows, resul return err } - rowLimit := 1000000 rowCount := 0 timeIndex := -1 @@ -225,7 +226,6 @@ func (e *sqlQueryEndpoint) transformToTimeSeries(query *Query, rows *core.Rows, return err } - rowLimit := 1000000 rowCount := 0 timeIndex := -1 metricIndex := -1 From 67c613a45a3ab3b15b587e6999e83a63d52a1582 Mon Sep 17 00:00:00 2001 From: Tobias Skarhed Date: Fri, 27 Jul 2018 13:29:57 +0200 Subject: [PATCH 186/786] Begin conversion --- public/app/core/specs/backend_srv.jest.ts | 39 +++++++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 public/app/core/specs/backend_srv.jest.ts diff --git a/public/app/core/specs/backend_srv.jest.ts b/public/app/core/specs/backend_srv.jest.ts new file mode 100644 index 00000000000..6281f3814ce --- /dev/null +++ b/public/app/core/specs/backend_srv.jest.ts @@ -0,0 +1,39 @@ +import { BackendSrv } from 'app/core/services/backend_srv'; +jest.mock('app/core/store'); + +describe('backend_srv', function() { + let _httpBackend = options => { + if (options.method === 'GET' && options.url === 'gateway-error') { + return Promise.reject({ status: 502 }); + } else if (options.method === 'POST') { + // return Promise.resolve({}); + } + return Promise.resolve({}); + }; + + let _backendSrv = new BackendSrv(_httpBackend, {}, {}, {}, {}); + + // beforeEach(angularMocks.module('grafana.core')); + // beforeEach(angularMocks.module('grafana.services')); + // beforeEach( + // angularMocks.inject(function($httpBackend, $http, backendSrv) { + // _httpBackend = $httpBackend; + // _backendSrv = backendSrv; + // }) + // ); + + describe('when handling errors', function() { + it('should return the http status code', function(done) { + // _httpBackend.whenGET('gateway-error').respond(502); + _backendSrv + .datasourceRequest({ + url: 'gateway-error', + }) + .catch(function(err) { + expect(err.status).toBe(502); + done(); + }); + // _httpBackend.flush(); + }); + }); +}); From b4ac3f2379e675439f571c308eb36581d4a39984 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Fri, 27 Jul 2018 13:33:50 +0200 Subject: [PATCH 187/786] update devenv datasources and dashboards for sql datasources --- devenv/dev-dashboards/datasource_tests_mssql_fakedata.json | 1 - devenv/dev-dashboards/datasource_tests_mssql_unittest.json | 1 - devenv/dev-dashboards/datasource_tests_mysql_fakedata.json | 1 - devenv/dev-dashboards/datasource_tests_mysql_unittest.json | 1 - devenv/dev-dashboards/datasource_tests_postgres_fakedata.json | 1 - devenv/dev-dashboards/datasource_tests_postgres_unittest.json | 1 - 6 files changed, 6 deletions(-) diff --git a/devenv/dev-dashboards/datasource_tests_mssql_fakedata.json b/devenv/dev-dashboards/datasource_tests_mssql_fakedata.json index 4350b5e44a8..e810a686134 100644 --- a/devenv/dev-dashboards/datasource_tests_mssql_fakedata.json +++ b/devenv/dev-dashboards/datasource_tests_mssql_fakedata.json @@ -16,7 +16,6 @@ "editable": true, "gnetId": null, "graphTooltip": 0, - "id": 203, "iteration": 1532618661457, "links": [], "panels": [ diff --git a/devenv/dev-dashboards/datasource_tests_mssql_unittest.json b/devenv/dev-dashboards/datasource_tests_mssql_unittest.json index 5c8eb8243a3..d47cfb0ad6e 100644 --- a/devenv/dev-dashboards/datasource_tests_mssql_unittest.json +++ b/devenv/dev-dashboards/datasource_tests_mssql_unittest.json @@ -64,7 +64,6 @@ "editable": true, "gnetId": null, "graphTooltip": 0, - "id": 35, "iteration": 1532618879985, "links": [], "panels": [ diff --git a/devenv/dev-dashboards/datasource_tests_mysql_fakedata.json b/devenv/dev-dashboards/datasource_tests_mysql_fakedata.json index cef8fd4783f..ebeb452fc4c 100644 --- a/devenv/dev-dashboards/datasource_tests_mysql_fakedata.json +++ b/devenv/dev-dashboards/datasource_tests_mysql_fakedata.json @@ -16,7 +16,6 @@ "editable": true, "gnetId": null, "graphTooltip": 0, - "id": 4, "iteration": 1532620738041, "links": [], "panels": [ diff --git a/devenv/dev-dashboards/datasource_tests_mysql_unittest.json b/devenv/dev-dashboards/datasource_tests_mysql_unittest.json index 2c20969da12..326114ec8ff 100644 --- a/devenv/dev-dashboards/datasource_tests_mysql_unittest.json +++ b/devenv/dev-dashboards/datasource_tests_mysql_unittest.json @@ -64,7 +64,6 @@ "editable": true, "gnetId": null, "graphTooltip": 0, - "id": 39, "iteration": 1532620354037, "links": [], "panels": [ diff --git a/devenv/dev-dashboards/datasource_tests_postgres_fakedata.json b/devenv/dev-dashboards/datasource_tests_postgres_fakedata.json index 1afa6e25df8..508cae86bc3 100644 --- a/devenv/dev-dashboards/datasource_tests_postgres_fakedata.json +++ b/devenv/dev-dashboards/datasource_tests_postgres_fakedata.json @@ -16,7 +16,6 @@ "editable": true, "gnetId": null, "graphTooltip": 0, - "id": 5, "iteration": 1532620601931, "links": [], "panels": [ diff --git a/devenv/dev-dashboards/datasource_tests_postgres_unittest.json b/devenv/dev-dashboards/datasource_tests_postgres_unittest.json index d7d5f238e85..85151089b7f 100644 --- a/devenv/dev-dashboards/datasource_tests_postgres_unittest.json +++ b/devenv/dev-dashboards/datasource_tests_postgres_unittest.json @@ -64,7 +64,6 @@ "editable": true, "gnetId": null, "graphTooltip": 0, - "id": 38, "iteration": 1532619575136, "links": [], "panels": [ From 55111c801fbdc74687d74136dc73daf2aa29131c Mon Sep 17 00:00:00 2001 From: Tobias Skarhed Date: Fri, 27 Jul 2018 13:41:07 +0200 Subject: [PATCH 188/786] Update test for local time --- .../plugins/panel/singlestat/specs/singlestat.jest.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/public/app/plugins/panel/singlestat/specs/singlestat.jest.ts b/public/app/plugins/panel/singlestat/specs/singlestat.jest.ts index 7e8915ca537..dd02b5c169c 100644 --- a/public/app/plugins/panel/singlestat/specs/singlestat.jest.ts +++ b/public/app/plugins/panel/singlestat/specs/singlestat.jest.ts @@ -23,6 +23,9 @@ describe('SingleStatCtrl', function() { SingleStatCtrl.prototype.dashboard = { isTimezoneUtc: jest.fn(() => true), }; + SingleStatCtrl.prototype.events = { + on: () => {}, + }; function singleStatScenario(desc, func) { describe(desc, function() { @@ -84,7 +87,7 @@ describe('SingleStatCtrl', function() { }); it('should set formatted value', function() { - expect(ctx.data.valueFormatted).toBe('2017-09-17 09:56:37'); + expect(moment(ctx.data.valueFormatted).isSame('2017-09-17 09:56:37')).toBe(true); }); }); @@ -235,7 +238,9 @@ describe('SingleStatCtrl', function() { singleStatScenario('with default values', function(ctx) { ctx.setup(function() { ctx.data = tableData; - ctx.ctrl.panel = {}; + ctx.ctrl.panel = { + emit: () => {}, + }; ctx.ctrl.panel.tableColumn = 'mean'; ctx.ctrl.panel.format = 'none'; }); From 1bb5a57036d435299bc287bb4e93eab92b77f7bd Mon Sep 17 00:00:00 2001 From: Patrick O'Carroll Date: Fri, 27 Jul 2018 13:45:16 +0200 Subject: [PATCH 189/786] frontend part with mock-team-list --- public/app/features/org/partials/profile.html | 99 +++++++++++-------- public/app/features/org/profile_ctrl.ts | 15 +++ 2 files changed, 73 insertions(+), 41 deletions(-) diff --git a/public/app/features/org/partials/profile.html b/public/app/features/org/partials/profile.html index 66e41fbb4b4..96540911290 100644 --- a/public/app/features/org/partials/profile.html +++ b/public/app/features/org/partials/profile.html @@ -3,53 +3,70 @@

    User Profile

    -
    + -
    - Name - -
    -
    - Email - +
    + Name + +
    +
    + Email +
    -
    - Username +
    + Username
    -
    - -
    - +
    + +
    + - + -

    Organizations

    +

    Teams

    +
    +
    - this.deleteTeam(team)} /> + this.deleteTeam(team)} />
    + + + + + + + + + + + + +
    NameEmail
    {{team.name}}{{team.email}}
    +
    + +

    Organizations

    - - - - - - - - - - - - - - - -
    NameRole
    {{org.name}}{{org.role}} - - Current - - - Select - -
    -
    - + + + + + + + + + + + + + + + +
    NameRole
    {{org.name}}{{org.role}} + + Current + + + Select + +
    +
    diff --git a/public/app/features/org/profile_ctrl.ts b/public/app/features/org/profile_ctrl.ts index 5c62a7a5fdb..1ac950699be 100644 --- a/public/app/features/org/profile_ctrl.ts +++ b/public/app/features/org/profile_ctrl.ts @@ -4,8 +4,10 @@ import { coreModule } from 'app/core/core'; export class ProfileCtrl { user: any; old_theme: any; + teams: any = []; orgs: any = []; userForm: any; + showTeamsList = false; showOrgsList = false; readonlyLoginFields = config.disableLoginForm; navModel: any; @@ -13,6 +15,7 @@ export class ProfileCtrl { /** @ngInject **/ constructor(private backendSrv, private contextSrv, private $location, navModelSrv) { this.getUser(); + this.getUserTeams(); this.getUserOrgs(); this.navModel = navModelSrv.getNav('profile', 'profile-settings', 0); } @@ -24,6 +27,18 @@ export class ProfileCtrl { }); } + getUserTeams() { + console.log(this.backendSrv.get('/api/teams')); + this.backendSrv.get('/api/user').then(teams => { + this.user.teams = [ + { name: 'Backend', email: 'backend@grafana.com', members: 2 }, + { name: 'Frontend', email: 'frontend@grafana.com', members: 2 }, + { name: 'Ops', email: 'ops@grafana.com', members: 2 }, + ]; + this.showTeamsList = this.user.teams.length > 1; + }); + } + getUserOrgs() { this.backendSrv.get('/api/user/orgs').then(orgs => { this.orgs = orgs; From 971e52ecc98126788066f0452aeaa7bf93f7baf2 Mon Sep 17 00:00:00 2001 From: Patrick O'Carroll Date: Fri, 27 Jul 2018 13:48:14 +0200 Subject: [PATCH 190/786] removed unused class from the deletebutton pr --- public/app/containers/Teams/TeamList.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/app/containers/Teams/TeamList.tsx b/public/app/containers/Teams/TeamList.tsx index b86763d8799..31406250cb3 100644 --- a/public/app/containers/Teams/TeamList.tsx +++ b/public/app/containers/Teams/TeamList.tsx @@ -88,7 +88,7 @@ export class TeamList extends React.Component {
    -
    +
    From ad26a319c50753053009fcb8b539cf118662e051 Mon Sep 17 00:00:00 2001 From: Sven Klemm Date: Fri, 27 Jul 2018 14:02:12 +0200 Subject: [PATCH 191/786] refactor schema query generation --- .../plugins/datasource/postgres/meta_query.ts | 55 ++++++++++++------- 1 file changed, 35 insertions(+), 20 deletions(-) diff --git a/public/app/plugins/datasource/postgres/meta_query.ts b/public/app/plugins/datasource/postgres/meta_query.ts index 64271c022cc..c2fc8647137 100644 --- a/public/app/plugins/datasource/postgres/meta_query.ts +++ b/public/app/plugins/datasource/postgres/meta_query.ts @@ -55,31 +55,46 @@ LIMIT 1 return query; } - buildTableQuery() { + buildSchemaConstraint() { let query = ` -SELECT quote_ident(table_name) -FROM information_schema.tables -WHERE - table_schema IN ( - SELECT CASE WHEN trim(unnest) = \'"$user"\' THEN user ELSE trim(unnest) END - FROM unnest(string_to_array(current_setting(\'search_path\'),\',\')) - ) -ORDER BY table_name`; +table_schema IN ( + SELECT CASE WHEN trim(unnest) = \'"$user"\' THEN user ELSE trim(unnest) END + FROM unnest(string_to_array(current_setting(\'search_path\'),\',\')) +)`; + return query; + } + + buildTableConstraint(table: string) { + let query = ''; + + // check for schema qualified table + if (table.includes('.')) { + let parts = table.split('.'); + query = 'table_schema = ' + this.quoteIdentAsLiteral(parts[0]); + query += ' AND table_name = ' + this.quoteIdentAsLiteral(parts[1]); + return query; + } else { + query = ` +table_schema IN ( + SELECT CASE WHEN trim(unnest) = \'"$user"\' THEN user ELSE trim(unnest) END + FROM unnest(string_to_array(current_setting(\'search_path\'),\',\')) +)`; + query += ' AND table_name = ' + this.quoteIdentAsLiteral(table); + + return query; + } + } + + buildTableQuery() { + let query = 'SELECT quote_ident(table_name) FROM information_schema.tables WHERE '; + query += this.buildSchemaConstraint(); + query += ' ORDER BY table_name'; return query; } buildColumnQuery(type?: string) { - let query = ` -SELECT quote_ident(column_name) -FROM information_schema.columns -WHERE - table_schema IN ( - SELECT CASE WHEN trim(unnest) = \'"$user"\' THEN user ELSE trim(unnest) END - FROM unnest(string_to_array(current_setting(\'search_path\'),\',\')) - LIMIT 1 - ) -`; - query += ' AND table_name = ' + this.quoteIdentAsLiteral(this.target.table); + let query = 'SELECT quote_ident(column_name) FROM information_schema.columns WHERE '; + query += this.buildTableConstraint(this.target.table); switch (type) { case 'time': { From 4e6168f3a331e5701e279305774413eca87499d4 Mon Sep 17 00:00:00 2001 From: Tobias Skarhed Date: Fri, 27 Jul 2018 14:22:48 +0200 Subject: [PATCH 192/786] Add async/await --- public/app/core/specs/backend_srv.jest.ts | 22 +++++++++------------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/public/app/core/specs/backend_srv.jest.ts b/public/app/core/specs/backend_srv.jest.ts index 6281f3814ce..2d62716622a 100644 --- a/public/app/core/specs/backend_srv.jest.ts +++ b/public/app/core/specs/backend_srv.jest.ts @@ -3,10 +3,9 @@ jest.mock('app/core/store'); describe('backend_srv', function() { let _httpBackend = options => { - if (options.method === 'GET' && options.url === 'gateway-error') { + console.log(options); + if (options.url === 'gateway-error') { return Promise.reject({ status: 502 }); - } else if (options.method === 'POST') { - // return Promise.resolve({}); } return Promise.resolve({}); }; @@ -22,17 +21,14 @@ describe('backend_srv', function() { // }) // ); - describe('when handling errors', function() { - it('should return the http status code', function(done) { + describe('when handling errors', () => { + it('should return the http status code', async () => { // _httpBackend.whenGET('gateway-error').respond(502); - _backendSrv - .datasourceRequest({ - url: 'gateway-error', - }) - .catch(function(err) { - expect(err.status).toBe(502); - done(); - }); + let res = await _backendSrv.datasourceRequest({ + url: 'gateway-error', + }); + console.log(res); + expect(res.status).toBe(502); // _httpBackend.flush(); }); }); From 2db4a54f75c7c1bd8a3a70ea0d4be50f88ab0552 Mon Sep 17 00:00:00 2001 From: Tobias Skarhed Date: Fri, 27 Jul 2018 14:40:56 +0200 Subject: [PATCH 193/786] Fix test --- public/app/plugins/panel/singlestat/specs/singlestat.jest.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/app/plugins/panel/singlestat/specs/singlestat.jest.ts b/public/app/plugins/panel/singlestat/specs/singlestat.jest.ts index dd02b5c169c..0480d0be5c3 100644 --- a/public/app/plugins/panel/singlestat/specs/singlestat.jest.ts +++ b/public/app/plugins/panel/singlestat/specs/singlestat.jest.ts @@ -87,7 +87,7 @@ describe('SingleStatCtrl', function() { }); it('should set formatted value', function() { - expect(moment(ctx.data.valueFormatted).isSame('2017-09-17 09:56:37')).toBe(true); + expect(moment(ctx.data.valueFormatted).valueOf()).toBe(1505634997000); }); }); From 766c23a1eb86d6ba47b2d61d9b72153089b73264 Mon Sep 17 00:00:00 2001 From: Tobias Skarhed Date: Fri, 27 Jul 2018 15:16:19 +0200 Subject: [PATCH 194/786] Fix emit errors --- public/app/plugins/panel/graph/specs/graph_ctrl.jest.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/public/app/plugins/panel/graph/specs/graph_ctrl.jest.ts b/public/app/plugins/panel/graph/specs/graph_ctrl.jest.ts index 3ebcf6cdf31..a0c7dd0ab9c 100644 --- a/public/app/plugins/panel/graph/specs/graph_ctrl.jest.ts +++ b/public/app/plugins/panel/graph/specs/graph_ctrl.jest.ts @@ -34,6 +34,9 @@ describe('GraphCtrl', () => { beforeEach(() => { ctx.ctrl = new GraphCtrl(scope, injector, {}); + ctx.ctrl.events = { + emit: () => {}, + }; ctx.ctrl.annotationsPromise = Promise.resolve({}); ctx.ctrl.updateTimeRange(); }); From b28a362635876bc321063127f0e3ddf3d599cb79 Mon Sep 17 00:00:00 2001 From: Sven Klemm Date: Sat, 21 Jul 2018 11:04:05 +0200 Subject: [PATCH 195/786] Use metric column as prefix If multiple value columns are returned and a metric column is returned aswell the metric column will be used as prefix for the series name --- docs/sources/features/datasources/postgres.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/sources/features/datasources/postgres.md b/docs/sources/features/datasources/postgres.md index f9af60a2efc..f3e52ed6652 100644 --- a/docs/sources/features/datasources/postgres.md +++ b/docs/sources/features/datasources/postgres.md @@ -101,7 +101,7 @@ The resulting table panel: If you set `Format as` to `Time series`, for use in Graph panel for example, then the query must return a column named `time` that returns either a sql datetime or any numeric datatype representing unix epoch. Any column except `time` and `metric` is treated as a value column. -You may return a column named `metric` that is used as metric name for the value column. +You may return a column named `metric` that is used as metric name for the value column. If you return multiple value columns and a column named `metric` then this column is used as prefix for the series name. **Example with `metric` column:** From f9d6c88a556142791bc6ba0af96ca46dd0dac037 Mon Sep 17 00:00:00 2001 From: Sven Klemm Date: Tue, 24 Jul 2018 18:31:47 +0200 Subject: [PATCH 196/786] add testcase for metric column as prefix --- pkg/tsdb/postgres/postgres_test.go | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/pkg/tsdb/postgres/postgres_test.go b/pkg/tsdb/postgres/postgres_test.go index 089829bf590..c7787929a9d 100644 --- a/pkg/tsdb/postgres/postgres_test.go +++ b/pkg/tsdb/postgres/postgres_test.go @@ -568,6 +568,31 @@ func TestPostgres(t *testing.T) { So(queryResult.Series[1].Name, ShouldEqual, "Metric B - value one") }) + Convey("When doing a metric query with metric column and multiple value columns", func() { + query := &tsdb.TsdbQuery{ + Queries: []*tsdb.Query{ + { + Model: simplejson.NewFromAny(map[string]interface{}{ + "rawSql": `SELECT $__timeEpoch(time), measurement as metric, "valueOne", "valueTwo" FROM metric_values ORDER BY 1`, + "format": "time_series", + }), + RefId: "A", + }, + }, + } + + resp, err := endpoint.Query(nil, nil, query) + So(err, ShouldBeNil) + queryResult := resp.Results["A"] + So(queryResult.Error, ShouldBeNil) + + So(len(queryResult.Series), ShouldEqual, 4) + So(queryResult.Series[0].Name, ShouldEqual, "Metric A valueOne") + So(queryResult.Series[1].Name, ShouldEqual, "Metric A valueTwo") + So(queryResult.Series[2].Name, ShouldEqual, "Metric B valueOne") + So(queryResult.Series[3].Name, ShouldEqual, "Metric B valueTwo") + }) + Convey("When doing a metric query grouping by time should return correct series", func() { query := &tsdb.TsdbQuery{ Queries: []*tsdb.Query{ From 7905c29875a29d230af476e41cb070b13bc9de73 Mon Sep 17 00:00:00 2001 From: Sven Klemm Date: Tue, 24 Jul 2018 19:25:48 +0200 Subject: [PATCH 197/786] adjust metric prefix code to sql engine refactor --- pkg/tsdb/sql_engine.go | 15 ++++++++++++++- .../postgres/partials/query.editor.html | 5 ++++- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/pkg/tsdb/sql_engine.go b/pkg/tsdb/sql_engine.go index 27ed37923a3..027f37fc243 100644 --- a/pkg/tsdb/sql_engine.go +++ b/pkg/tsdb/sql_engine.go @@ -229,6 +229,8 @@ func (e *sqlQueryEndpoint) transformToTimeSeries(query *Query, rows *core.Rows, rowCount := 0 timeIndex := -1 metricIndex := -1 + metricPrefix := false + var metricPrefixValue string // check columns of resultset: a column named time is mandatory // the first text column is treated as metric name unless a column named metric is present @@ -256,6 +258,11 @@ func (e *sqlQueryEndpoint) transformToTimeSeries(query *Query, rows *core.Rows, } } + // use metric column as prefix with multiple value columns + if metricIndex != -1 && len(columnNames) > 3 { + metricPrefix = true + } + if timeIndex == -1 { return fmt.Errorf("Found no column named %s", strings.Join(e.timeColumnNames, " or ")) } @@ -301,7 +308,11 @@ func (e *sqlQueryEndpoint) transformToTimeSeries(query *Query, rows *core.Rows, if metricIndex >= 0 { if columnValue, ok := values[metricIndex].(string); ok { - metric = columnValue + if metricPrefix { + metricPrefixValue = columnValue + } else { + metric = columnValue + } } else { return fmt.Errorf("Column metric must be of type %s. metric column name: %s type: %s but datatype is %T", strings.Join(e.metricColumnTypes, ", "), columnNames[metricIndex], columnTypes[metricIndex].DatabaseTypeName(), values[metricIndex]) } @@ -318,6 +329,8 @@ func (e *sqlQueryEndpoint) transformToTimeSeries(query *Query, rows *core.Rows, if metricIndex == -1 { metric = col + } else if metricPrefix { + metric = metricPrefixValue + " " + col } series, exist := pointsBySeries[metric] diff --git a/public/app/plugins/datasource/postgres/partials/query.editor.html b/public/app/plugins/datasource/postgres/partials/query.editor.html index 26392c17356..b7c12471f52 100644 --- a/public/app/plugins/datasource/postgres/partials/query.editor.html +++ b/public/app/plugins/datasource/postgres/partials/query.editor.html @@ -40,7 +40,10 @@
    Time series:
     - return column named time (UTC in seconds or timestamp)
     - return column(s) with numeric datatype as values
    -- (Optional: return column named metric to represent the series name. If no column named metric is found the column name of the value column is used as series name)
    +Optional: 
    +  - return column named metric to represent the series name. 
    +  - If multiple value columns are returned the metric column is used as prefix. 
    +  - If no column named metric is found the column name of the value column is used as series name
     
     Table:
     - return any set of columns
    
    From 2f6b302375bbe7c562e6df09760f1f4b495b2715 Mon Sep 17 00:00:00 2001
    From: Tobias Skarhed 
    Date: Fri, 27 Jul 2018 15:51:56 +0200
    Subject: [PATCH 198/786] Test passing. Remove Karma
    
    ---
     public/app/core/specs/backend_srv.jest.ts  | 23 +++++-----------
     public/app/core/specs/backend_srv_specs.ts | 31 ----------------------
     2 files changed, 7 insertions(+), 47 deletions(-)
     delete mode 100644 public/app/core/specs/backend_srv_specs.ts
    
    diff --git a/public/app/core/specs/backend_srv.jest.ts b/public/app/core/specs/backend_srv.jest.ts
    index 2d62716622a..c65464aa875 100644
    --- a/public/app/core/specs/backend_srv.jest.ts
    +++ b/public/app/core/specs/backend_srv.jest.ts
    @@ -12,24 +12,15 @@ describe('backend_srv', function() {
     
       let _backendSrv = new BackendSrv(_httpBackend, {}, {}, {}, {});
     
    -  //   beforeEach(angularMocks.module('grafana.core'));
    -  //   beforeEach(angularMocks.module('grafana.services'));
    -  //   beforeEach(
    -  //     angularMocks.inject(function($httpBackend, $http, backendSrv) {
    -  //       _httpBackend = $httpBackend;
    -  //       _backendSrv = backendSrv;
    -  //     })
    -  //   );
    -
       describe('when handling errors', () => {
         it('should return the http status code', async () => {
    -      //   _httpBackend.whenGET('gateway-error').respond(502);
    -      let res = await _backendSrv.datasourceRequest({
    -        url: 'gateway-error',
    -      });
    -      console.log(res);
    -      expect(res.status).toBe(502);
    -      //   _httpBackend.flush();
    +      try {
    +        await _backendSrv.datasourceRequest({
    +          url: 'gateway-error',
    +        });
    +      } catch (err) {
    +        expect(err.status).toBe(502);
    +      }
         });
       });
     });
    diff --git a/public/app/core/specs/backend_srv_specs.ts b/public/app/core/specs/backend_srv_specs.ts
    deleted file mode 100644
    index 74b058b98c8..00000000000
    --- a/public/app/core/specs/backend_srv_specs.ts
    +++ /dev/null
    @@ -1,31 +0,0 @@
    -import { describe, beforeEach, it, expect, angularMocks } from 'test/lib/common';
    -import 'app/core/services/backend_srv';
    -
    -describe('backend_srv', function() {
    -  var _backendSrv;
    -  var _httpBackend;
    -
    -  beforeEach(angularMocks.module('grafana.core'));
    -  beforeEach(angularMocks.module('grafana.services'));
    -  beforeEach(
    -    angularMocks.inject(function($httpBackend, $http, backendSrv) {
    -      _httpBackend = $httpBackend;
    -      _backendSrv = backendSrv;
    -    })
    -  );
    -
    -  describe('when handling errors', function() {
    -    it('should return the http status code', function(done) {
    -      _httpBackend.whenGET('gateway-error').respond(502);
    -      _backendSrv
    -        .datasourceRequest({
    -          url: 'gateway-error',
    -        })
    -        .catch(function(err) {
    -          expect(err.status).to.be(502);
    -          done();
    -        });
    -      _httpBackend.flush();
    -    });
    -  });
    -});
    
    From c11d0f5cc6289b708d1e0d7c072de7eb6b1b8422 Mon Sep 17 00:00:00 2001
    From: Tobias Skarhed 
    Date: Fri, 27 Jul 2018 15:52:22 +0200
    Subject: [PATCH 199/786] Remove lo
    
    ---
     public/app/core/specs/backend_srv.jest.ts | 1 -
     1 file changed, 1 deletion(-)
    
    diff --git a/public/app/core/specs/backend_srv.jest.ts b/public/app/core/specs/backend_srv.jest.ts
    index c65464aa875..b19bd117766 100644
    --- a/public/app/core/specs/backend_srv.jest.ts
    +++ b/public/app/core/specs/backend_srv.jest.ts
    @@ -3,7 +3,6 @@ jest.mock('app/core/store');
     
     describe('backend_srv', function() {
       let _httpBackend = options => {
    -    console.log(options);
         if (options.url === 'gateway-error') {
           return Promise.reject({ status: 502 });
         }
    
    From 895b4b40eee4af0ee79b0935856ff1c532ebeb94 Mon Sep 17 00:00:00 2001
    From: Worty <6840978+Worty@users.noreply.github.com>
    Date: Fri, 27 Jul 2018 16:26:04 +0200
    Subject: [PATCH 200/786] correct volume unit
    
    ---
     public/app/core/specs/kbn.jest.ts |  2 +-
     public/app/core/utils/kbn.ts      | 36 +++++++++++++++----------------
     2 files changed, 19 insertions(+), 19 deletions(-)
    
    diff --git a/public/app/core/specs/kbn.jest.ts b/public/app/core/specs/kbn.jest.ts
    index 68945068043..9c62990615c 100644
    --- a/public/app/core/specs/kbn.jest.ts
    +++ b/public/app/core/specs/kbn.jest.ts
    @@ -402,7 +402,7 @@ describe('duration', function() {
     describe('volume', function() {
       it('1000m3', function() {
         var str = kbn.valueFormats['m3'](1000, 1, null);
    -    expect(str).toBe('1000.0 m3');
    +    expect(str).toBe('1000.0 m³');
       });
     });
     
    diff --git a/public/app/core/utils/kbn.ts b/public/app/core/utils/kbn.ts
    index 4fc4829811f..74ef2a9e874 100644
    --- a/public/app/core/utils/kbn.ts
    +++ b/public/app/core/utils/kbn.ts
    @@ -572,9 +572,9 @@ kbn.valueFormats.accG = kbn.formatBuilders.fixedUnit('g');
     // Volume
     kbn.valueFormats.litre = kbn.formatBuilders.decimalSIPrefix('L');
     kbn.valueFormats.mlitre = kbn.formatBuilders.decimalSIPrefix('L', -1);
    -kbn.valueFormats.m3 = kbn.formatBuilders.fixedUnit('m3');
    -kbn.valueFormats.Nm3 = kbn.formatBuilders.fixedUnit('Nm3');
    -kbn.valueFormats.dm3 = kbn.formatBuilders.fixedUnit('dm3');
    +kbn.valueFormats.m3 = kbn.formatBuilders.fixedUnit('m³');
    +kbn.valueFormats.Nm3 = kbn.formatBuilders.fixedUnit('Nm³');
    +kbn.valueFormats.dm3 = kbn.formatBuilders.fixedUnit('dm³');
     kbn.valueFormats.gallons = kbn.formatBuilders.fixedUnit('gal');
     
     // Flow
    @@ -605,14 +605,14 @@ kbn.valueFormats.radsvh = kbn.formatBuilders.decimalSIPrefix('Sv/h');
     // Concentration
     kbn.valueFormats.ppm = kbn.formatBuilders.fixedUnit('ppm');
     kbn.valueFormats.conppb = kbn.formatBuilders.fixedUnit('ppb');
    -kbn.valueFormats.conngm3 = kbn.formatBuilders.fixedUnit('ng/m3');
    -kbn.valueFormats.conngNm3 = kbn.formatBuilders.fixedUnit('ng/Nm3');
    -kbn.valueFormats.conμgm3 = kbn.formatBuilders.fixedUnit('μg/m3');
    -kbn.valueFormats.conμgNm3 = kbn.formatBuilders.fixedUnit('μg/Nm3');
    -kbn.valueFormats.conmgm3 = kbn.formatBuilders.fixedUnit('mg/m3');
    -kbn.valueFormats.conmgNm3 = kbn.formatBuilders.fixedUnit('mg/Nm3');
    -kbn.valueFormats.congm3 = kbn.formatBuilders.fixedUnit('g/m3');
    -kbn.valueFormats.congNm3 = kbn.formatBuilders.fixedUnit('g/Nm3');
    +kbn.valueFormats.conngm3 = kbn.formatBuilders.fixedUnit('ng/m³');
    +kbn.valueFormats.conngNm3 = kbn.formatBuilders.fixedUnit('ng/Nm³');
    +kbn.valueFormats.conμgm3 = kbn.formatBuilders.fixedUnit('μg/m³');
    +kbn.valueFormats.conμgNm3 = kbn.formatBuilders.fixedUnit('μg/Nm³');
    +kbn.valueFormats.conmgm3 = kbn.formatBuilders.fixedUnit('mg/m³');
    +kbn.valueFormats.conmgNm3 = kbn.formatBuilders.fixedUnit('mg/Nm³');
    +kbn.valueFormats.congm3 = kbn.formatBuilders.fixedUnit('g/m³');
    +kbn.valueFormats.congNm3 = kbn.formatBuilders.fixedUnit('g/Nm³');
     
     // Time
     kbn.valueFormats.hertz = kbn.formatBuilders.decimalSIPrefix('Hz');
    @@ -1119,13 +1119,13 @@ kbn.getUnitFormats = function() {
             { text: 'parts-per-million (ppm)', value: 'ppm' },
             { text: 'parts-per-billion (ppb)', value: 'conppb' },
             { text: 'nanogram per cubic metre (ng/m3)', value: 'conngm3' },
    -        { text: 'nanogram per normal cubic metre (ng/Nm3)', value: 'conngNm3' },
    -        { text: 'microgram per cubic metre (μg/m3)', value: 'conμgm3' },
    -        { text: 'microgram per normal cubic metre (μg/Nm3)', value: 'conμgNm3' },
    -        { text: 'milligram per cubic metre (mg/m3)', value: 'conmgm3' },
    -        { text: 'milligram per normal cubic metre (mg/Nm3)', value: 'conmgNm3' },
    -        { text: 'gram per cubic metre (g/m3)', value: 'congm3' },
    -        { text: 'gram per normal cubic metre (g/Nm3)', value: 'congNm3' },
    +        { text: 'nanogram per normal cubic metre (ng/Nm³)', value: 'conngNm3' },
    +        { text: 'microgram per cubic metre (μg/m³)', value: 'conμgm3' },
    +        { text: 'microgram per normal cubic metre (μg/Nm³)', value: 'conμgNm3' },
    +        { text: 'milligram per cubic metre (mg/m³)', value: 'conmgm3' },
    +        { text: 'milligram per normal cubic metre (mg/Nm³)', value: 'conmgNm3' },
    +        { text: 'gram per cubic metre (g/m³)', value: 'congm3' },
    +        { text: 'gram per normal cubic metre (g/Nm³)', value: 'congNm3' },
           ],
         },
       ];
    
    From 26f709e87ea5d551b46f3b15909165aee732e298 Mon Sep 17 00:00:00 2001
    From: Tobias Skarhed 
    Date: Fri, 27 Jul 2018 16:45:03 +0200
    Subject: [PATCH 201/786] Karm to Jest
    
    ---
     ...map_ctrl_specs.ts => heatmap_ctrl.jest.ts} | 44 ++++++++++---------
     1 file changed, 24 insertions(+), 20 deletions(-)
     rename public/app/plugins/panel/heatmap/specs/{heatmap_ctrl_specs.ts => heatmap_ctrl.jest.ts} (61%)
    
    diff --git a/public/app/plugins/panel/heatmap/specs/heatmap_ctrl_specs.ts b/public/app/plugins/panel/heatmap/specs/heatmap_ctrl.jest.ts
    similarity index 61%
    rename from public/app/plugins/panel/heatmap/specs/heatmap_ctrl_specs.ts
    rename to public/app/plugins/panel/heatmap/specs/heatmap_ctrl.jest.ts
    index 98055ccf52d..70449763856 100644
    --- a/public/app/plugins/panel/heatmap/specs/heatmap_ctrl_specs.ts
    +++ b/public/app/plugins/panel/heatmap/specs/heatmap_ctrl.jest.ts
    @@ -1,26 +1,30 @@
    -import { describe, beforeEach, it, expect, angularMocks } from '../../../../../test/lib/common';
    -
     import moment from 'moment';
     import { HeatmapCtrl } from '../heatmap_ctrl';
    -import helpers from '../../../../../test/specs/helpers';
     
     describe('HeatmapCtrl', function() {
    -  var ctx = new helpers.ControllerTestContext();
    +  let ctx = {};
     
    -  beforeEach(angularMocks.module('grafana.services'));
    -  beforeEach(angularMocks.module('grafana.controllers'));
    -  beforeEach(
    -    angularMocks.module(function($compileProvider) {
    -      $compileProvider.preAssignBindingsEnabled(true);
    -    })
    -  );
    +  let $injector = {
    +      get: () => {}
    +  };
     
    -  beforeEach(ctx.providePhase());
    -  beforeEach(ctx.createPanelController(HeatmapCtrl));
    -  beforeEach(() => {
    -    ctx.ctrl.annotationsPromise = Promise.resolve({});
    -    ctx.ctrl.updateTimeRange();
    -  });
    +  let $scope = {
    +    $on: () => {},
    +    events: {
    +        on: () => {}
    +    }
    +  };
    +
    +HeatmapCtrl.prototype.panel = {
    +    events: {
    +        on: () => {},
    +        emit: () => {}
    +    }
    +};
    +
    +    beforeEach(() => {
    +        ctx.ctrl = new HeatmapCtrl($scope, $injector, {});
    +    });
     
       describe('when time series are outside range', function() {
         beforeEach(function() {
    @@ -36,7 +40,7 @@ describe('HeatmapCtrl', function() {
         });
     
         it('should set datapointsOutside', function() {
    -      expect(ctx.ctrl.dataWarning.title).to.be('Data points outside time range');
    +      expect(ctx.ctrl.dataWarning.title).toBe('Data points outside time range');
         });
       });
     
    @@ -61,7 +65,7 @@ describe('HeatmapCtrl', function() {
         });
     
         it('should set datapointsOutside', function() {
    -      expect(ctx.ctrl.dataWarning).to.be(null);
    +      expect(ctx.ctrl.dataWarning).toBe(null);
         });
       });
     
    @@ -72,7 +76,7 @@ describe('HeatmapCtrl', function() {
         });
     
         it('should set datapointsCount warning', function() {
    -      expect(ctx.ctrl.dataWarning.title).to.be('No data points');
    +      expect(ctx.ctrl.dataWarning.title).toBe('No data points');
         });
       });
     });
    
    From 805dc3542f780c57f477c61cf9cf475515aa3760 Mon Sep 17 00:00:00 2001
    From: Tobias Skarhed 
    Date: Fri, 27 Jul 2018 16:46:41 +0200
    Subject: [PATCH 202/786] Remove extra mock
    
    ---
     .../panel/heatmap/specs/heatmap_ctrl.jest.ts  | 21 ++++++++-----------
     1 file changed, 9 insertions(+), 12 deletions(-)
    
    diff --git a/public/app/plugins/panel/heatmap/specs/heatmap_ctrl.jest.ts b/public/app/plugins/panel/heatmap/specs/heatmap_ctrl.jest.ts
    index 70449763856..800c2518f9a 100644
    --- a/public/app/plugins/panel/heatmap/specs/heatmap_ctrl.jest.ts
    +++ b/public/app/plugins/panel/heatmap/specs/heatmap_ctrl.jest.ts
    @@ -5,26 +5,23 @@ describe('HeatmapCtrl', function() {
       let ctx = {};
     
       let $injector = {
    -      get: () => {}
    +    get: () => {},
       };
     
       let $scope = {
         $on: () => {},
    -    events: {
    -        on: () => {}
    -    }
       };
     
    -HeatmapCtrl.prototype.panel = {
    +  HeatmapCtrl.prototype.panel = {
         events: {
    -        on: () => {},
    -        emit: () => {}
    -    }
    -};
    +      on: () => {},
    +      emit: () => {},
    +    },
    +  };
     
    -    beforeEach(() => {
    -        ctx.ctrl = new HeatmapCtrl($scope, $injector, {});
    -    });
    +  beforeEach(() => {
    +    ctx.ctrl = new HeatmapCtrl($scope, $injector, {});
    +  });
     
       describe('when time series are outside range', function() {
         beforeEach(function() {
    
    From bc9b6ddefe9c982b778d699c7c445db081982fbd Mon Sep 17 00:00:00 2001
    From: Sven Klemm 
    Date: Fri, 27 Jul 2018 17:14:27 +0200
    Subject: [PATCH 203/786] document metric column prefix for mysql and mssql
    
    ---
     docs/sources/features/datasources/mssql.md | 2 +-
     docs/sources/features/datasources/mysql.md | 2 +-
     2 files changed, 2 insertions(+), 2 deletions(-)
    
    diff --git a/docs/sources/features/datasources/mssql.md b/docs/sources/features/datasources/mssql.md
    index d4d5cc6d73e..bcb965dda74 100644
    --- a/docs/sources/features/datasources/mssql.md
    +++ b/docs/sources/features/datasources/mssql.md
    @@ -148,7 +148,7 @@ The resulting table panel:
     
     ## Time series queries
     
    -If you set `Format as` to `Time series`, for use in Graph panel for example, then the query must must have a column named `time` that returns either a sql datetime or any numeric datatype representing unix epoch in seconds. You may return a column named `metric` that is used as metric name for the value column. Any column except `time` and `metric` is treated as a value column. If you omit the `metric` column, tha name of the value column will be the metric name. You may select multiple value columns, each will have its name as metric.
    +If you set `Format as` to `Time series`, for use in Graph panel for example, then the query must must have a column named `time` that returns either a sql datetime or any numeric datatype representing unix epoch in seconds. You may return a column named `metric` that is used as metric name for the value column. Any column except `time` and `metric` is treated as a value column. If you omit the `metric` column, tha name of the value column will be the metric name. You may select multiple value columns, each will have its name as metric. If you return multiple value columns and a column named `metric` then this column is used as prefix for the series name.
     
     **Example database table:**
     
    diff --git a/docs/sources/features/datasources/mysql.md b/docs/sources/features/datasources/mysql.md
    index ce50053c7ea..c6e620eb08b 100644
    --- a/docs/sources/features/datasources/mysql.md
    +++ b/docs/sources/features/datasources/mysql.md
    @@ -103,7 +103,7 @@ The resulting table panel:
     
     If you set `Format as` to `Time series`, for use in Graph panel for example, then the query must return a column named `time` that returns either a sql datetime or any numeric datatype representing unix epoch.
     Any column except `time` and `metric` is treated as a value column.
    -You may return a column named `metric` that is used as metric name for the value column.
    +You may return a column named `metric` that is used as metric name for the value column. If you return multiple value columns and a column named `metric` then this column is used as prefix for the series name.
     
     **Example with `metric` column:**
     
    
    From 036647ae35b9e6799d5af9b984a47a5907c40d6a Mon Sep 17 00:00:00 2001
    From: Sven Klemm 
    Date: Fri, 27 Jul 2018 17:18:45 +0200
    Subject: [PATCH 204/786] document metric column prefix in query editor
    
    ---
     .../app/plugins/datasource/mssql/partials/query.editor.html | 6 ++++--
     .../app/plugins/datasource/mysql/partials/query.editor.html | 5 ++++-
     2 files changed, 8 insertions(+), 3 deletions(-)
    
    diff --git a/public/app/plugins/datasource/mssql/partials/query.editor.html b/public/app/plugins/datasource/mssql/partials/query.editor.html
    index ddc24475d60..397a35164c0 100644
    --- a/public/app/plugins/datasource/mssql/partials/query.editor.html
    +++ b/public/app/plugins/datasource/mssql/partials/query.editor.html
    @@ -39,9 +39,11 @@
     	
    Time series:
     - return column named time (in UTC), as a unix time stamp or any sql native date data type. You can use the macros below.
    -- optional: return column named metric to represent the series names.
     - any other columns returned will be the time point values.
    -- if multiple value columns are present and a metric column is provided. the series name will be the combination of "MetricName - ValueColumnName".
    +Optional:
    +  - return column named metric to represent the series name.
    +  - If multiple value columns are returned the metric column is used as prefix.
    +  - If no column named metric is found the column name of the value column is used as series name
     
     Table:
     - return any set of columns
    diff --git a/public/app/plugins/datasource/mysql/partials/query.editor.html b/public/app/plugins/datasource/mysql/partials/query.editor.html
    index df68982fcfa..d4be22fc3e9 100644
    --- a/public/app/plugins/datasource/mysql/partials/query.editor.html
    +++ b/public/app/plugins/datasource/mysql/partials/query.editor.html
    @@ -40,7 +40,10 @@
     		
    Time series:
     - return column named time or time_sec (in UTC), as a unix time stamp or any sql native date data type. You can use the macros below.
     - return column(s) with numeric datatype as values
    -- (Optional: return column named metric to represent the series name. If no column named metric is found the column name of the value column is used as series name)
    +Optional:
    +  - return column named metric to represent the series name.
    +  - If multiple value columns are returned the metric column is used as prefix.
    +  - If no column named metric is found the column name of the value column is used as series name
     
     Table:
     - return any set of columns
    
    From e487fabcd56f5a04b8fa5a6cba6a020855f2d062 Mon Sep 17 00:00:00 2001
    From: Sven Klemm 
    Date: Fri, 27 Jul 2018 17:54:51 +0200
    Subject: [PATCH 205/786] add metric column prefix test for mysql
    
    ---
     pkg/tsdb/mysql/mysql_test.go | 25 +++++++++++++++++++++++++
     1 file changed, 25 insertions(+)
    
    diff --git a/pkg/tsdb/mysql/mysql_test.go b/pkg/tsdb/mysql/mysql_test.go
    index 3b4e283b726..9947c23498b 100644
    --- a/pkg/tsdb/mysql/mysql_test.go
    +++ b/pkg/tsdb/mysql/mysql_test.go
    @@ -634,6 +634,31 @@ func TestMySQL(t *testing.T) {
     				So(queryResult.Series[1].Name, ShouldEqual, "Metric B - value one")
     			})
     
    +			Convey("When doing a metric query with metric column and multiple value columns", func() {
    +				query := &tsdb.TsdbQuery{
    +					Queries: []*tsdb.Query{
    +						{
    +							Model: simplejson.NewFromAny(map[string]interface{}{
    +								"rawSql": `SELECT $__time(time), measurement as metric, valueOne, valueTwo FROM metric_values ORDER BY 1,2`,
    +								"format": "time_series",
    +							}),
    +							RefId: "A",
    +						},
    +					},
    +				}
    +
    +				resp, err := endpoint.Query(nil, nil, query)
    +				So(err, ShouldBeNil)
    +				queryResult := resp.Results["A"]
    +				So(queryResult.Error, ShouldBeNil)
    +
    +				So(len(queryResult.Series), ShouldEqual, 4)
    +				So(queryResult.Series[0].Name, ShouldEqual, "Metric A valueOne")
    +				So(queryResult.Series[1].Name, ShouldEqual, "Metric A valueTwo")
    +				So(queryResult.Series[2].Name, ShouldEqual, "Metric B valueOne")
    +				So(queryResult.Series[3].Name, ShouldEqual, "Metric B valueTwo")
    +			})
    +
     			Convey("When doing a metric query grouping by time should return correct series", func() {
     				query := &tsdb.TsdbQuery{
     					Queries: []*tsdb.Query{
    
    From 3aa4790979cf457a26754afd67f5235fc3345f62 Mon Sep 17 00:00:00 2001
    From: Sven Klemm 
    Date: Fri, 27 Jul 2018 18:13:19 +0200
    Subject: [PATCH 206/786] add tests for metric column prefix to mssql
    
    ---
     pkg/tsdb/mssql/mssql_test.go | 25 +++++++++++++++++++++++++
     1 file changed, 25 insertions(+)
    
    diff --git a/pkg/tsdb/mssql/mssql_test.go b/pkg/tsdb/mssql/mssql_test.go
    index 86484cb9d5e..8e3d617ca09 100644
    --- a/pkg/tsdb/mssql/mssql_test.go
    +++ b/pkg/tsdb/mssql/mssql_test.go
    @@ -610,6 +610,31 @@ func TestMSSQL(t *testing.T) {
     				So(queryResult.Series[1].Name, ShouldEqual, "valueTwo")
     			})
     
    +			Convey("When doing a metric query with metric column and multiple value columns", func() {
    +				query := &tsdb.TsdbQuery{
    +					Queries: []*tsdb.Query{
    +						{
    +							Model: simplejson.NewFromAny(map[string]interface{}{
    +								"rawSql": "SELECT $__timeEpoch(time), measurement AS metric, valueOne, valueTwo FROM metric_values ORDER BY 1",
    +								"format": "time_series",
    +							}),
    +							RefId: "A",
    +						},
    +					},
    +				}
    +
    +				resp, err := endpoint.Query(nil, nil, query)
    +				So(err, ShouldBeNil)
    +				queryResult := resp.Results["A"]
    +				So(queryResult.Error, ShouldBeNil)
    +
    +				So(len(queryResult.Series), ShouldEqual, 4)
    +				So(queryResult.Series[0].Name, ShouldEqual, "Metric A valueOne")
    +				So(queryResult.Series[1].Name, ShouldEqual, "Metric A valueTwo")
    +				So(queryResult.Series[2].Name, ShouldEqual, "Metric B valueOne")
    +				So(queryResult.Series[3].Name, ShouldEqual, "Metric B valueTwo")
    +			})
    +
     			Convey("Given a stored procedure that takes @from and @to in epoch time", func() {
     				sql := `
     						IF object_id('sp_test_epoch') IS NOT NULL
    
    From 20b2b344f6b230887f9f0625cc10485ccc29dde1 Mon Sep 17 00:00:00 2001
    From: Marcus Efraimsson 
    Date: Sat, 28 Jul 2018 11:31:30 +0200
    Subject: [PATCH 207/786] mssql: add logo
    
    ---
     .../datasource/mssql/img/sql_server_logo.svg  | 115 ++++++++++++++++++
     .../app/plugins/datasource/mssql/plugin.json  |   4 +-
     2 files changed, 117 insertions(+), 2 deletions(-)
     create mode 100644 public/app/plugins/datasource/mssql/img/sql_server_logo.svg
    
    diff --git a/public/app/plugins/datasource/mssql/img/sql_server_logo.svg b/public/app/plugins/datasource/mssql/img/sql_server_logo.svg
    new file mode 100644
    index 00000000000..7fb7859c8ac
    --- /dev/null
    +++ b/public/app/plugins/datasource/mssql/img/sql_server_logo.svg
    @@ -0,0 +1,115 @@
    +
    +
    +
    +
    +  
    +    
    +    
    +      
    +      
    +    
    +    
    +    
    +      
    +      
    +    
    +    
    +    
    +      
    +      
    +    
    +  
    +  
    +    
    +      
    +        image/svg+xml
    +        
    +        
    +      
    +    
    +  
    +  
    +    
    +      
    +        
    +        
    +        
    +      
    +    
    +  
    +
    diff --git a/public/app/plugins/datasource/mssql/plugin.json b/public/app/plugins/datasource/mssql/plugin.json
    index 65ef82511cd..ac5ea49ebe9 100644
    --- a/public/app/plugins/datasource/mssql/plugin.json
    +++ b/public/app/plugins/datasource/mssql/plugin.json
    @@ -10,8 +10,8 @@
           "url": "https://grafana.com"
         },
         "logos": {
    -      "small": "",
    -      "large": ""
    +      "small": "img/sql_server_logo.svg",
    +      "large": "img/sql_server_logo.svg"
         }
       },
     
    
    From 6ca7a0397514596aa07baf462e4ad1e109f2a9b4 Mon Sep 17 00:00:00 2001
    From: Sven Klemm 
    Date: Sat, 28 Jul 2018 12:53:36 +0200
    Subject: [PATCH 208/786] consistent nameing fro group and select
    
    ---
     .../plugins/datasource/postgres/meta_query.ts |  2 +-
     .../postgres/partials/query.editor.html       |  8 +--
     .../datasource/postgres/postgres_query.ts     | 28 ++++-----
     .../plugins/datasource/postgres/query_ctrl.ts | 62 +++++++++----------
     .../postgres/specs/postgres_query.jest.ts     | 14 ++---
     5 files changed, 57 insertions(+), 57 deletions(-)
    
    diff --git a/public/app/plugins/datasource/postgres/meta_query.ts b/public/app/plugins/datasource/postgres/meta_query.ts
    index c2fc8647137..fd29121313d 100644
    --- a/public/app/plugins/datasource/postgres/meta_query.ts
    +++ b/public/app/plugins/datasource/postgres/meta_query.ts
    @@ -111,7 +111,7 @@ table_schema IN (
             query += ' AND column_name <> ' + this.quoteIdentAsLiteral(this.target.timeColumn);
             break;
           }
    -      case 'groupby': {
    +      case 'group': {
             query += " AND data_type IN ('text','char','varchar')";
             break;
           }
    diff --git a/public/app/plugins/datasource/postgres/partials/query.editor.html b/public/app/plugins/datasource/postgres/partials/query.editor.html
    index 68711f3ea0b..3d3b7c43388 100644
    --- a/public/app/plugins/datasource/postgres/partials/query.editor.html
    +++ b/public/app/plugins/datasource/postgres/partials/query.editor.html
    @@ -31,7 +31,7 @@
     
         
    -
    +
    - + handle-event="ctrl.onGroupPartEvent(part, $index, $event)">
    - +
    diff --git a/public/app/plugins/datasource/postgres/postgres_query.ts b/public/app/plugins/datasource/postgres/postgres_query.ts index 3c1b1b681b4..a6f45dbcee8 100644 --- a/public/app/plugins/datasource/postgres/postgres_query.ts +++ b/public/app/plugins/datasource/postgres/postgres_query.ts @@ -15,7 +15,7 @@ export default class PostgresQuery { target.timeColumn = target.timeColumn || 'time'; target.metricColumn = target.metricColumn || 'none'; - target.groupBy = target.groupBy || []; + target.group = target.group || []; target.where = target.where || [{ type: 'macro', name: '$__timeFilter', params: [] }]; target.select = target.select || [[{ type: 'column', params: ['value'] }]]; @@ -51,8 +51,8 @@ export default class PostgresQuery { return "'" + value.replace("'", "''") + "'"; } - hasGroupByTime() { - return _.find(this.target.groupBy, (g: any) => g.type === 'time'); + hasTimeGroup() { + return _.find(this.target.group, (g: any) => g.type === 'time'); } hasMetricColumn() { @@ -93,7 +93,7 @@ export default class PostgresQuery { } buildTimeColumn() { - let timeGroup = this.hasGroupByTime(); + let timeGroup = this.hasTimeGroup(); let query; if (timeGroup) { @@ -201,24 +201,24 @@ export default class PostgresQuery { return query; } - buildGroupByClause() { + buildGroupClause() { let query = ''; - let groupBySection = ''; + let groupSection = ''; - for (let i = 0; i < this.target.groupBy.length; i++) { - let part = this.target.groupBy[i]; + for (let i = 0; i < this.target.group.length; i++) { + let part = this.target.group[i]; if (i > 0) { - groupBySection += ', '; + groupSection += ', '; } if (part.type === 'time') { - groupBySection += '1'; + groupSection += '1'; } else { - groupBySection += part.params[0]; + groupSection += part.params[0]; } } - if (groupBySection.length) { - query = '\nGROUP BY ' + groupBySection; + if (groupSection.length) { + query = '\nGROUP BY ' + groupSection; if (this.hasMetricColumn()) { query += ',2'; } @@ -238,7 +238,7 @@ export default class PostgresQuery { query += '\nFROM ' + this.target.table; query += this.buildWhereClause(); - query += this.buildGroupByClause(); + query += this.buildGroupClause(); query += '\nORDER BY 1'; diff --git a/public/app/plugins/datasource/postgres/query_ctrl.ts b/public/app/plugins/datasource/postgres/query_ctrl.ts index 97db612e7db..62bd1c63fab 100644 --- a/public/app/plugins/datasource/postgres/query_ctrl.ts +++ b/public/app/plugins/datasource/postgres/query_ctrl.ts @@ -33,10 +33,10 @@ export class PostgresQueryCtrl extends QueryCtrl { timeColumnSegment: any; metricColumnSegment: any; selectMenu: any[]; - selectModels: SqlPart[][]; - groupByParts: SqlPart[][]; - whereParts: SqlPart[][]; - groupByAdd: any; + selectParts: SqlPart[][]; + groupParts: SqlPart[]; + whereParts: SqlPart[]; + groupAdd: any; /** @ngInject **/ constructor($scope, $injector, private templateSrv, private $q, private uiSegmentSrv) { @@ -69,22 +69,22 @@ export class PostgresQueryCtrl extends QueryCtrl { this.buildSelectMenu(); this.whereAdd = this.uiSegmentSrv.newPlusButton(); - this.groupByAdd = this.uiSegmentSrv.newPlusButton(); + this.groupAdd = this.uiSegmentSrv.newPlusButton(); this.panelCtrl.events.on('data-received', this.onDataReceived.bind(this), $scope); this.panelCtrl.events.on('data-error', this.onDataError.bind(this), $scope); } updateProjection() { - this.selectModels = _.map(this.target.select, function(parts: any) { + this.selectParts = _.map(this.target.select, function(parts: any) { return _.map(parts, sqlPart.create).filter(n => n); }); this.whereParts = _.map(this.target.where, sqlPart.create).filter(n => n); - this.groupByParts = _.map(this.target.groupBy, sqlPart.create).filter(n => n); + this.groupParts = _.map(this.target.group, sqlPart.create).filter(n => n); } updatePersistedParts() { - this.target.select = _.map(this.selectModels, function(selectParts) { + this.target.select = _.map(this.selectParts, function(selectParts) { return _.map(selectParts, function(part: any) { return { type: part.def.type, params: part.params }; }); @@ -92,7 +92,7 @@ export class PostgresQueryCtrl extends QueryCtrl { this.target.where = _.map(this.whereParts, function(part: any) { return { type: part.def.type, name: part.name, params: part.params }; }); - this.target.groupBy = _.map(this.groupByParts, function(part: any) { + this.target.group = _.map(this.groupParts, function(part: any) { return { type: part.def.type, params: part.params }; }); } @@ -216,12 +216,12 @@ export class PostgresQueryCtrl extends QueryCtrl { let parts = _.map(selectParts, function(part: any) { return sqlPart.create({ type: part.def.type, params: _.clone(part.params) }); }); - this.selectModels.push(parts); + this.selectParts.push(parts); break; case 'aggregate': // add group by if no group by yet - if (this.target.groupBy.length === 0) { - this.addGroupBy('time', '1m'); + if (this.target.group.length === 0) { + this.addGroup('time', '1m'); } case 'special': let index = _.findIndex(selectParts, (p: any) => p.def.type === item.value); @@ -256,9 +256,9 @@ export class PostgresQueryCtrl extends QueryCtrl { removeSelectPart(selectParts, part) { if (part.def.type === 'column') { // remove all parts of column unless its last column - if (this.selectModels.length > 1) { - let modelsIndex = _.indexOf(this.selectModels, selectParts); - this.selectModels.splice(modelsIndex, 1); + if (this.selectParts.length > 1) { + let modelsIndex = _.indexOf(this.selectParts, selectParts); + this.selectParts.splice(modelsIndex, 1); } } else { let partIndex = _.indexOf(selectParts, part); @@ -299,7 +299,7 @@ export class PostgresQueryCtrl extends QueryCtrl { } } - handleGroupByPartEvent(part, index, evt) { + onGroupPartEvent(part, index, evt) { switch (evt.name) { case 'get-param-options': { return this.datasource @@ -312,7 +312,7 @@ export class PostgresQueryCtrl extends QueryCtrl { break; } case 'action': { - this.removeGroupBy(part, index); + this.removeGroup(part, index); this.panelCtrl.refresh(); break; } @@ -322,7 +322,7 @@ export class PostgresQueryCtrl extends QueryCtrl { } } - addGroupBy(partType, value) { + addGroup(partType, value) { let params = [value]; if (partType === 'time') { params = ['1m', 'none']; @@ -331,13 +331,13 @@ export class PostgresQueryCtrl extends QueryCtrl { if (partType === 'time') { // put timeGroup at start - this.groupByParts.splice(0, 0, partModel); + this.groupParts.splice(0, 0, partModel); } else { - this.groupByParts.push(partModel); + this.groupParts.push(partModel); } // add aggregates when adding group by - for (let selectParts of this.selectModels) { + for (let selectParts of this.selectParts) { if (!selectParts.some(part => part.def.type === 'aggregate')) { let aggregate = sqlPart.create({ type: 'aggregate', params: ['avg'] }); selectParts.splice(1, 0, aggregate); @@ -351,10 +351,10 @@ export class PostgresQueryCtrl extends QueryCtrl { this.updatePersistedParts(); } - removeGroupBy(part, index) { + removeGroup(part, index) { if (part.def.type === 'time') { // remove aggregations - this.selectModels = _.map(this.selectModels, (s: any) => { + this.selectParts = _.map(this.selectParts, (s: any) => { return _.filter(s, (part: any) => { if (part.def.type === 'aggregate') { return false; @@ -364,7 +364,7 @@ export class PostgresQueryCtrl extends QueryCtrl { }); } - this.groupByParts.splice(index, 1); + this.groupParts.splice(index, 1); this.updatePersistedParts(); } @@ -429,12 +429,12 @@ export class PostgresQueryCtrl extends QueryCtrl { this.panelCtrl.refresh(); } - getGroupByOptions() { + getGroupOptions() { return this.datasource - .metricFindQuery(this.metaBuilder.buildColumnQuery('groupby')) + .metricFindQuery(this.metaBuilder.buildColumnQuery('group')) .then(tags => { var options = []; - if (!this.queryModel.hasGroupByTime()) { + if (!this.queryModel.hasTimeGroup()) { options.push(this.uiSegmentSrv.newSegment({ type: 'time', value: 'time(1m,none)' })); } for (let tag of tags) { @@ -445,14 +445,14 @@ export class PostgresQueryCtrl extends QueryCtrl { .catch(this.handleQueryError.bind(this)); } - groupByAction() { - switch (this.groupByAdd.value) { + onGroupAction() { + switch (this.groupAdd.value) { default: { - this.addGroupBy(this.groupByAdd.type, this.groupByAdd.value); + this.addGroup(this.groupAdd.type, this.groupAdd.value); } } - this.resetPlusButton(this.groupByAdd); + this.resetPlusButton(this.groupAdd); this.panelCtrl.refresh(); } diff --git a/public/app/plugins/datasource/postgres/specs/postgres_query.jest.ts b/public/app/plugins/datasource/postgres/specs/postgres_query.jest.ts index 33d997d2d0a..00ac5ed0e56 100644 --- a/public/app/plugins/datasource/postgres/specs/postgres_query.jest.ts +++ b/public/app/plugins/datasource/postgres/specs/postgres_query.jest.ts @@ -27,12 +27,12 @@ describe('PostgresQuery', function() { describe('When generating time column SQL with group by time', function() { let query = new PostgresQuery( - { timeColumn: 'time', groupBy: [{ type: 'time', params: ['5m', 'none'] }] }, + { timeColumn: 'time', group: [{ type: 'time', params: ['5m', 'none'] }] }, templateSrv ); expect(query.buildTimeColumn()).toBe('$__timeGroup(time,5m)'); - query = new PostgresQuery({ timeColumn: 'time', groupBy: [{ type: 'time', params: ['5m', 'NULL'] }] }, templateSrv); + query = new PostgresQuery({ timeColumn: 'time', group: [{ type: 'time', params: ['5m', 'NULL'] }] }, templateSrv); expect(query.buildTimeColumn()).toBe('$__timeGroup(time,5m,NULL)'); }); @@ -114,13 +114,13 @@ describe('PostgresQuery', function() { }); describe('When generating GROUP BY clause', function() { - let query = new PostgresQuery({ groupBy: [], metricColumn: 'none' }, templateSrv); + let query = new PostgresQuery({ group: [], metricColumn: 'none' }, templateSrv); - expect(query.buildGroupByClause()).toBe(''); - query.target.groupBy = [{ type: 'time', params: ['5m'] }]; - expect(query.buildGroupByClause()).toBe('\nGROUP BY 1'); + expect(query.buildGroupClause()).toBe(''); + query.target.group = [{ type: 'time', params: ['5m'] }]; + expect(query.buildGroupClause()).toBe('\nGROUP BY 1'); query.target.metricColumn = 'm'; - expect(query.buildGroupByClause()).toBe('\nGROUP BY 1,2'); + expect(query.buildGroupClause()).toBe('\nGROUP BY 1,2'); }); describe('When generating complete statement', function() { From 5327580939fbd242fc25da9e17e50f6ff8f02098 Mon Sep 17 00:00:00 2001 From: Sven Klemm Date: Sat, 28 Jul 2018 21:30:08 +0200 Subject: [PATCH 209/786] refactor column function handling --- .../postgres/partials/query.editor.html | 2 +- .../datasource/postgres/postgres_query.ts | 15 ++++++--- .../plugins/datasource/postgres/query_ctrl.ts | 31 ++++++++++++++++--- .../postgres/specs/postgres_query.jest.ts | 17 ++++++---- .../plugins/datasource/postgres/sql_part.ts | 4 +-- 5 files changed, 52 insertions(+), 17 deletions(-) diff --git a/public/app/plugins/datasource/postgres/partials/query.editor.html b/public/app/plugins/datasource/postgres/partials/query.editor.html index 3d3b7c43388..58d2415479a 100644 --- a/public/app/plugins/datasource/postgres/partials/query.editor.html +++ b/public/app/plugins/datasource/postgres/partials/query.editor.html @@ -46,7 +46,7 @@
    diff --git a/public/app/plugins/datasource/postgres/postgres_query.ts b/public/app/plugins/datasource/postgres/postgres_query.ts index a6f45dbcee8..eb51449dd39 100644 --- a/public/app/plugins/datasource/postgres/postgres_query.ts +++ b/public/app/plugins/datasource/postgres/postgres_query.ts @@ -135,7 +135,7 @@ export default class PostgresQuery { query = columnName.params[0]; let aggregate = _.find(column, (g: any) => g.type === 'aggregate'); - let special = _.find(column, (g: any) => g.type === 'special'); + let special = _.find(column, (g: any) => g.type === 'window'); if (aggregate) { if (special) { @@ -155,9 +155,13 @@ export default class PostgresQuery { } let over = overParts.join(' '); + let curr: string; + let prev: string; switch (special.params[0]) { case 'increase': - query = query + ' - lag(' + query + ') OVER (' + over + ')'; + curr = query; + prev = 'lag(' + curr + ') OVER (' + over + ')'; + query = '(CASE WHEN ' + curr + ' >= ' + prev + ' THEN ' + curr + ' - ' + prev + ' ELSE ' + curr + ' END)'; break; case 'rate': let timeColumn = this.target.timeColumn; @@ -165,11 +169,14 @@ export default class PostgresQuery { timeColumn = 'min(' + timeColumn + ')'; } - let curr = query; - let prev = 'lag(' + curr + ') OVER (' + over + ')'; + curr = query; + prev = 'lag(' + curr + ') OVER (' + over + ')'; query = '(CASE WHEN ' + curr + ' >= ' + prev + ' THEN ' + curr + ' - ' + prev + ' ELSE ' + curr + ' END)'; query += '/extract(epoch from ' + timeColumn + ' - lag(' + timeColumn + ') OVER (' + over + '))'; break; + default: + query = special.params[0] + '(' + query + ') OVER (' + over + ')'; + break; } } diff --git a/public/app/plugins/datasource/postgres/query_ctrl.ts b/public/app/plugins/datasource/postgres/query_ctrl.ts index 62bd1c63fab..88ef587037d 100644 --- a/public/app/plugins/datasource/postgres/query_ctrl.ts +++ b/public/app/plugins/datasource/postgres/query_ctrl.ts @@ -99,8 +99,28 @@ export class PostgresQueryCtrl extends QueryCtrl { buildSelectMenu() { this.selectMenu = [ - { text: 'Aggregate', value: 'aggregate' }, - { text: 'Special', value: 'special' }, + { + text: 'Aggregate Functions', + value: 'aggregate', + submenu: [ + { text: 'Average', value: 'avg' }, + { text: 'Count', value: 'count' }, + { text: 'Maximum', value: 'max' }, + { text: 'Minimum', value: 'min' }, + { text: 'Sum', value: 'sum' }, + { text: 'Standard deviation', value: 'stddev' }, + { text: 'Variance', value: 'variance' }, + ], + }, + { + text: 'Window Functions', + value: 'window', + submenu: [ + { text: 'Increase', value: 'increase' }, + { text: 'Rate', value: 'rate' }, + { text: 'Sum', value: 'sum' }, + ], + }, { text: 'Alias', value: 'alias' }, { text: 'Column', value: 'column' }, ]; @@ -207,8 +227,11 @@ export class PostgresQueryCtrl extends QueryCtrl { }; } - addSelectPart(selectParts, item) { + addSelectPart(selectParts, item, subItem) { let partModel = sqlPart.create({ type: item.value }); + if (subItem) { + partModel.params = [subItem.value]; + } let addAlias = false; switch (item.value) { @@ -223,7 +246,7 @@ export class PostgresQueryCtrl extends QueryCtrl { if (this.target.group.length === 0) { this.addGroup('time', '1m'); } - case 'special': + case 'window': let index = _.findIndex(selectParts, (p: any) => p.def.type === item.value); if (index !== -1) { selectParts[index] = partModel; diff --git a/public/app/plugins/datasource/postgres/specs/postgres_query.jest.ts b/public/app/plugins/datasource/postgres/specs/postgres_query.jest.ts index 00ac5ed0e56..c589eb3c43c 100644 --- a/public/app/plugins/datasource/postgres/specs/postgres_query.jest.ts +++ b/public/app/plugins/datasource/postgres/specs/postgres_query.jest.ts @@ -61,9 +61,11 @@ describe('PostgresQuery', function() { column = [ { type: 'column', params: ['v'] }, { type: 'alias', params: ['a'] }, - { type: 'special', params: ['increase'] }, + { type: 'window', params: ['increase'] }, ]; - expect(query.buildValueColumn(column)).toBe('v - lag(v) OVER (ORDER BY time) AS "a"'); + expect(query.buildValueColumn(column)).toBe( + '(CASE WHEN v >= lag(v) OVER (ORDER BY time) THEN v - lag(v) OVER (ORDER BY time) ELSE v END) AS "a"' + ); }); describe('When generating value column SQL with metric column', function() { @@ -83,17 +85,20 @@ describe('PostgresQuery', function() { column = [ { type: 'column', params: ['v'] }, { type: 'alias', params: ['a'] }, - { type: 'special', params: ['increase'] }, + { type: 'window', params: ['increase'] }, ]; - expect(query.buildValueColumn(column)).toBe('v - lag(v) OVER (PARTITION BY host ORDER BY time) AS "a"'); + expect(query.buildValueColumn(column)).toBe( + '(CASE WHEN v >= lag(v) OVER (PARTITION BY host ORDER BY time) THEN v - lag(v) OVER (PARTITION BY host ORDER BY time) ELSE v END) AS "a"' + ); column = [ { type: 'column', params: ['v'] }, { type: 'alias', params: ['a'] }, { type: 'aggregate', params: ['max'] }, - { type: 'special', params: ['increase'] }, + { type: 'window', params: ['increase'] }, ]; expect(query.buildValueColumn(column)).toBe( - 'max(v ORDER BY time) - lag(max(v ORDER BY time)) OVER (PARTITION BY host) AS "a"' + '(CASE WHEN max(v ORDER BY time) >= lag(max(v ORDER BY time)) OVER (PARTITION BY host) ' + + 'THEN max(v ORDER BY time) - lag(max(v ORDER BY time)) OVER (PARTITION BY host) ELSE max(v ORDER BY time) END) AS "a"' ); }); diff --git a/public/app/plugins/datasource/postgres/sql_part.ts b/public/app/plugins/datasource/postgres/sql_part.ts index b0265a645ed..487ddb50276 100644 --- a/public/app/plugins/datasource/postgres/sql_part.ts +++ b/public/app/plugins/datasource/postgres/sql_part.ts @@ -76,13 +76,13 @@ register({ }); register({ - type: 'special', + type: 'window', style: 'label', params: [ { name: 'function', type: 'string', - options: ['increase', 'rate'], + options: ['increase', 'rate', 'sum'], }, ], defaultParams: ['increase'], From 412bb6acab9bd594747e8483b801e1e5cf338b78 Mon Sep 17 00:00:00 2001 From: Sven Klemm Date: Sun, 29 Jul 2018 13:30:21 +0200 Subject: [PATCH 210/786] refactor function handling in query builder --- .../postgres/partials/query.editor.html | 6 +-- .../datasource/postgres/postgres_query.ts | 17 +++++-- .../plugins/datasource/postgres/query_ctrl.ts | 50 +++++++++++++++---- .../plugins/datasource/postgres/sql_part.ts | 27 +++++++++- 4 files changed, 81 insertions(+), 19 deletions(-) diff --git a/public/app/plugins/datasource/postgres/partials/query.editor.html b/public/app/plugins/datasource/postgres/partials/query.editor.html index 58d2415479a..0c053388034 100644 --- a/public/app/plugins/datasource/postgres/partials/query.editor.html +++ b/public/app/plugins/datasource/postgres/partials/query.editor.html @@ -66,7 +66,7 @@
    - +
    @@ -83,12 +83,12 @@ + handle-event="ctrl.handleGroupPartEvent(part, $index, $event)">
    - +
    diff --git a/public/app/plugins/datasource/postgres/postgres_query.ts b/public/app/plugins/datasource/postgres/postgres_query.ts index eb51449dd39..4cd3ddefaa2 100644 --- a/public/app/plugins/datasource/postgres/postgres_query.ts +++ b/public/app/plugins/datasource/postgres/postgres_query.ts @@ -134,14 +134,21 @@ export default class PostgresQuery { let columnName = _.find(column, (g: any) => g.type === 'column'); query = columnName.params[0]; - let aggregate = _.find(column, (g: any) => g.type === 'aggregate'); + let aggregate = _.find(column, (g: any) => g.type === 'aggregate' || g.type === 'percentile'); let special = _.find(column, (g: any) => g.type === 'window'); if (aggregate) { - if (special) { - query = aggregate.params[0] + '(' + query + ' ORDER BY ' + this.target.timeColumn + ')'; - } else { - query = aggregate.params[0] + '(' + query + ')'; + switch (aggregate.type) { + case 'aggregate': + if (special) { + query = aggregate.params[0] + '(' + query + ' ORDER BY ' + this.target.timeColumn + ')'; + } else { + query = aggregate.params[0] + '(' + query + ')'; + } + break; + case 'percentile': + query = aggregate.params[0] + '(' + aggregate.params[1] + ') WITHIN GROUP (ORDER BY ' + query + ')'; + break; } } diff --git a/public/app/plugins/datasource/postgres/query_ctrl.ts b/public/app/plugins/datasource/postgres/query_ctrl.ts index 88ef587037d..0191d46065f 100644 --- a/public/app/plugins/datasource/postgres/query_ctrl.ts +++ b/public/app/plugins/datasource/postgres/query_ctrl.ts @@ -112,6 +112,14 @@ export class PostgresQueryCtrl extends QueryCtrl { { text: 'Variance', value: 'variance' }, ], }, + { + text: 'Ordered-Set Aggregate Functions', + value: 'percentile', + submenu: [ + { text: 'Percentile (continuous)', value: 'percentile_cont' }, + { text: 'Percentile (discrete)', value: 'percentile_disc' }, + ], + }, { text: 'Window Functions', value: 'window', @@ -121,9 +129,9 @@ export class PostgresQueryCtrl extends QueryCtrl { { text: 'Sum', value: 'sum' }, ], }, - { text: 'Alias', value: 'alias' }, - { text: 'Column', value: 'column' }, ]; + this.selectMenu.push({ text: 'Alias', value: 'alias' }); + this.selectMenu.push({ text: 'Column', value: 'column' }); } toggleEditorMode() { @@ -241,15 +249,17 @@ export class PostgresQueryCtrl extends QueryCtrl { }); this.selectParts.push(parts); break; + case 'percentile': + partModel.params.push('0.95'); case 'aggregate': // add group by if no group by yet if (this.target.group.length === 0) { this.addGroup('time', '1m'); } - case 'window': - let index = _.findIndex(selectParts, (p: any) => p.def.type === item.value); - if (index !== -1) { - selectParts[index] = partModel; + let aggIndex = _.findIndex(selectParts, (p: any) => p.def.type === 'aggregate' || p.def.type === 'percentile'); + if (aggIndex !== -1) { + // replace current aggregation + selectParts[aggIndex] = partModel; } else { selectParts.splice(1, 0, partModel); } @@ -257,6 +267,26 @@ export class PostgresQueryCtrl extends QueryCtrl { addAlias = true; } break; + case 'window': + let windowIndex = _.findIndex(selectParts, (p: any) => p.def.type === 'window'); + if (windowIndex !== -1) { + // replace current window function + selectParts[windowIndex] = partModel; + } else { + let aggIndex = _.findIndex( + selectParts, + (p: any) => p.def.type === 'aggregate' || p.def.type === 'percentile' + ); + if (aggIndex !== -1) { + selectParts.splice(aggIndex + 1, 0, partModel); + } else { + selectParts.splice(1, 0, partModel); + } + } + if (!_.find(selectParts, (p: any) => p.def.type === 'alias')) { + addAlias = true; + } + break; case 'alias': addAlias = true; break; @@ -322,7 +352,7 @@ export class PostgresQueryCtrl extends QueryCtrl { } } - onGroupPartEvent(part, index, evt) { + handleGroupPartEvent(part, index, evt) { switch (evt.name) { case 'get-param-options': { return this.datasource @@ -379,7 +409,7 @@ export class PostgresQueryCtrl extends QueryCtrl { // remove aggregations this.selectParts = _.map(this.selectParts, (s: any) => { return _.filter(s, (part: any) => { - if (part.def.type === 'aggregate') { + if (part.def.type === 'aggregate' || part.def.type === 'percentile') { return false; } return true; @@ -436,7 +466,7 @@ export class PostgresQueryCtrl extends QueryCtrl { return this.$q.when(options); } - whereAddAction(part, index) { + addWhereAction(part, index) { switch (this.whereAdd.type) { case 'macro': { this.whereParts.push(sqlPart.create({ type: 'macro', name: this.whereAdd.value, params: [] })); @@ -468,7 +498,7 @@ export class PostgresQueryCtrl extends QueryCtrl { .catch(this.handleQueryError.bind(this)); } - onGroupAction() { + addGroupAction() { switch (this.groupAdd.value) { default: { this.addGroup(this.groupAdd.type, this.groupAdd.value); diff --git a/public/app/plugins/datasource/postgres/sql_part.ts b/public/app/plugins/datasource/postgres/sql_part.ts index 487ddb50276..9cf0bd8f425 100644 --- a/public/app/plugins/datasource/postgres/sql_part.ts +++ b/public/app/plugins/datasource/postgres/sql_part.ts @@ -45,10 +45,35 @@ register({ register({ type: 'aggregate', style: 'label', - params: [{ name: 'name', type: 'string', dynamicLookup: true }], + params: [ + { + name: 'name', + type: 'string', + options: ['avg', 'count', 'min', 'max', 'sum', 'stddev', 'variance'], + }, + ], defaultParams: ['avg'], }); +register({ + type: 'percentile', + label: 'Aggregate:', + style: 'label', + params: [ + { + name: 'name', + type: 'string', + options: ['percentile_cont', 'percentile_disc'], + }, + { + name: 'fraction', + type: 'number', + options: ['0.5', '0.75', '0.9', '0.95', '0.99'], + }, + ], + defaultParams: ['percentile_cont', '0.95'], +}); + register({ type: 'alias', style: 'label', From 26ea88252bee35e45d113ccc8333217a4df7e2dc Mon Sep 17 00:00:00 2001 From: Sven Klemm Date: Sun, 29 Jul 2018 15:00:13 +0200 Subject: [PATCH 211/786] add first and last support --- .../datasource/postgres/postgres_query.ts | 13 +++- .../plugins/datasource/postgres/query_ctrl.ts | 75 +++++++++++-------- 2 files changed, 52 insertions(+), 36 deletions(-) diff --git a/public/app/plugins/datasource/postgres/postgres_query.ts b/public/app/plugins/datasource/postgres/postgres_query.ts index 4cd3ddefaa2..4c9bd862658 100644 --- a/public/app/plugins/datasource/postgres/postgres_query.ts +++ b/public/app/plugins/datasource/postgres/postgres_query.ts @@ -138,16 +138,21 @@ export default class PostgresQuery { let special = _.find(column, (g: any) => g.type === 'window'); if (aggregate) { + let func = aggregate.params[0]; switch (aggregate.type) { case 'aggregate': - if (special) { - query = aggregate.params[0] + '(' + query + ' ORDER BY ' + this.target.timeColumn + ')'; + if (func === 'first' || func === 'last') { + query = func + '(' + query + ',' + this.target.timeColumn + ')'; } else { - query = aggregate.params[0] + '(' + query + ')'; + if (special) { + query = func + '(' + query + ' ORDER BY ' + this.target.timeColumn + ')'; + } else { + query = func + '(' + query + ')'; + } } break; case 'percentile': - query = aggregate.params[0] + '(' + aggregate.params[1] + ') WITHIN GROUP (ORDER BY ' + query + ')'; + query = func + '(' + aggregate.params[1] + ') WITHIN GROUP (ORDER BY ' + query + ')'; break; } } diff --git a/public/app/plugins/datasource/postgres/query_ctrl.ts b/public/app/plugins/datasource/postgres/query_ctrl.ts index 0191d46065f..181a3a9234c 100644 --- a/public/app/plugins/datasource/postgres/query_ctrl.ts +++ b/public/app/plugins/datasource/postgres/query_ctrl.ts @@ -98,38 +98,49 @@ export class PostgresQueryCtrl extends QueryCtrl { } buildSelectMenu() { - this.selectMenu = [ - { - text: 'Aggregate Functions', - value: 'aggregate', - submenu: [ - { text: 'Average', value: 'avg' }, - { text: 'Count', value: 'count' }, - { text: 'Maximum', value: 'max' }, - { text: 'Minimum', value: 'min' }, - { text: 'Sum', value: 'sum' }, - { text: 'Standard deviation', value: 'stddev' }, - { text: 'Variance', value: 'variance' }, - ], - }, - { - text: 'Ordered-Set Aggregate Functions', - value: 'percentile', - submenu: [ - { text: 'Percentile (continuous)', value: 'percentile_cont' }, - { text: 'Percentile (discrete)', value: 'percentile_disc' }, - ], - }, - { - text: 'Window Functions', - value: 'window', - submenu: [ - { text: 'Increase', value: 'increase' }, - { text: 'Rate', value: 'rate' }, - { text: 'Sum', value: 'sum' }, - ], - }, - ]; + this.selectMenu = []; + let aggregates = { + text: 'Aggregate Functions', + value: 'aggregate', + submenu: [ + { text: 'Average', value: 'avg' }, + { text: 'Count', value: 'count' }, + { text: 'Maximum', value: 'max' }, + { text: 'Minimum', value: 'min' }, + { text: 'Sum', value: 'sum' }, + { text: 'Standard deviation', value: 'stddev' }, + { text: 'Variance', value: 'variance' }, + ], + }; + + // first and last are timescaledb specific + aggregates.submenu.push({ text: 'First', value: 'first' }); + aggregates.submenu.push({ text: 'Last', value: 'last' }); + + this.selectMenu.push(aggregates); + + // ordered set aggregates require postgres 9.4+ + let aggregates2 = { + text: 'Ordered-Set Aggregate Functions', + value: 'percentile', + submenu: [ + { text: 'Percentile (continuous)', value: 'percentile_cont' }, + { text: 'Percentile (discrete)', value: 'percentile_disc' }, + ], + }; + this.selectMenu.push(aggregates2); + + let windows = { + text: 'Window Functions', + value: 'window', + submenu: [ + { text: 'Increase', value: 'increase' }, + { text: 'Rate', value: 'rate' }, + { text: 'Sum', value: 'sum' }, + ], + }; + this.selectMenu.push(windows); + this.selectMenu.push({ text: 'Alias', value: 'alias' }); this.selectMenu.push({ text: 'Column', value: 'column' }); } From ace999b13fe82a750bc1fc722b558eb7471837de Mon Sep 17 00:00:00 2001 From: Sven Klemm Date: Sun, 29 Jul 2018 15:56:22 +0200 Subject: [PATCH 212/786] rename special to windows --- .../app/plugins/datasource/postgres/postgres_query.ts | 10 +++++----- public/app/plugins/datasource/postgres/query_ctrl.ts | 11 ++++++----- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/public/app/plugins/datasource/postgres/postgres_query.ts b/public/app/plugins/datasource/postgres/postgres_query.ts index 4c9bd862658..9715665fd4b 100644 --- a/public/app/plugins/datasource/postgres/postgres_query.ts +++ b/public/app/plugins/datasource/postgres/postgres_query.ts @@ -135,7 +135,7 @@ export default class PostgresQuery { query = columnName.params[0]; let aggregate = _.find(column, (g: any) => g.type === 'aggregate' || g.type === 'percentile'); - let special = _.find(column, (g: any) => g.type === 'window'); + let windows = _.find(column, (g: any) => g.type === 'window'); if (aggregate) { let func = aggregate.params[0]; @@ -144,7 +144,7 @@ export default class PostgresQuery { if (func === 'first' || func === 'last') { query = func + '(' + query + ',' + this.target.timeColumn + ')'; } else { - if (special) { + if (windows) { query = func + '(' + query + ' ORDER BY ' + this.target.timeColumn + ')'; } else { query = func + '(' + query + ')'; @@ -157,7 +157,7 @@ export default class PostgresQuery { } } - if (special) { + if (windows) { let overParts = []; if (this.hasMetricColumn()) { overParts.push('PARTITION BY ' + this.target.metricColumn); @@ -169,7 +169,7 @@ export default class PostgresQuery { let over = overParts.join(' '); let curr: string; let prev: string; - switch (special.params[0]) { + switch (windows.params[0]) { case 'increase': curr = query; prev = 'lag(' + curr + ') OVER (' + over + ')'; @@ -187,7 +187,7 @@ export default class PostgresQuery { query += '/extract(epoch from ' + timeColumn + ' - lag(' + timeColumn + ') OVER (' + over + '))'; break; default: - query = special.params[0] + '(' + query + ') OVER (' + over + ')'; + query = windows.params[0] + '(' + query + ') OVER (' + over + ')'; break; } } diff --git a/public/app/plugins/datasource/postgres/query_ctrl.ts b/public/app/plugins/datasource/postgres/query_ctrl.ts index 181a3a9234c..b15a9773436 100644 --- a/public/app/plugins/datasource/postgres/query_ctrl.ts +++ b/public/app/plugins/datasource/postgres/query_ctrl.ts @@ -246,6 +246,10 @@ export class PostgresQueryCtrl extends QueryCtrl { }; } + findAggregateIndex(selectParts) { + return _.findIndex(selectParts, (p: any) => p.def.type === 'aggregate' || p.def.type === 'percentile'); + } + addSelectPart(selectParts, item, subItem) { let partModel = sqlPart.create({ type: item.value }); if (subItem) { @@ -267,7 +271,7 @@ export class PostgresQueryCtrl extends QueryCtrl { if (this.target.group.length === 0) { this.addGroup('time', '1m'); } - let aggIndex = _.findIndex(selectParts, (p: any) => p.def.type === 'aggregate' || p.def.type === 'percentile'); + let aggIndex = this.findAggregateIndex(selectParts); if (aggIndex !== -1) { // replace current aggregation selectParts[aggIndex] = partModel; @@ -284,10 +288,7 @@ export class PostgresQueryCtrl extends QueryCtrl { // replace current window function selectParts[windowIndex] = partModel; } else { - let aggIndex = _.findIndex( - selectParts, - (p: any) => p.def.type === 'aggregate' || p.def.type === 'percentile' - ); + let aggIndex = this.findAggregateIndex(selectParts); if (aggIndex !== -1) { selectParts.splice(aggIndex + 1, 0, partModel); } else { From e37e8cb38c649796db57a39868d4c3c79ddab9fd Mon Sep 17 00:00:00 2001 From: Jan Garaj Date: Mon, 30 Jul 2018 08:02:16 +0100 Subject: [PATCH 213/786] Add missing tls_skip_verify_insecure (#12748) --- conf/defaults.ini | 1 + 1 file changed, 1 insertion(+) diff --git a/conf/defaults.ini b/conf/defaults.ini index 5faba3ea7bd..6c27886c649 100644 --- a/conf/defaults.ini +++ b/conf/defaults.ini @@ -311,6 +311,7 @@ token_url = api_url = team_ids = allowed_organizations = +tls_skip_verify_insecure = false #################################### Basic Auth ########################## [auth.basic] From 13a7b638bcc90ff6abcf00a388d8dfbedf01a8b2 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Mon, 30 Jul 2018 10:19:51 +0200 Subject: [PATCH 214/786] changelog: add notes about closing #12747 [skip ci] --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ad1b63234e9..4a2c3c7a0af 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,7 @@ * **Cloudwatch**: Improved error handling [#12489](https://github.com/grafana/grafana/issues/12489), thx [@mtanda](https://github.com/mtanda) * **Table**: Adjust header contrast for the light theme [#12668](https://github.com/grafana/grafana/issues/12668) * **Elasticsearch**: For alerting/backend, support having index name to the right of pattern in index pattern [#12731](https://github.com/grafana/grafana/issues/12731) +* **Auth**: Fix overriding tls_skip_verify_insecure using environment variable [#12747](https://github.com/grafana/grafana/issues/12747), thx [@jangaraj](https://github.com/jangaraj) # 5.2.2 (2018-07-25) From e4983cba2fc17de8523814b7126e5c2d858ac569 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Mon, 30 Jul 2018 10:21:22 +0200 Subject: [PATCH 215/786] changelog: update [skip ci] --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4a2c3c7a0af..b8f5bced972 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,7 +24,7 @@ * **Cloudwatch**: Improved error handling [#12489](https://github.com/grafana/grafana/issues/12489), thx [@mtanda](https://github.com/mtanda) * **Table**: Adjust header contrast for the light theme [#12668](https://github.com/grafana/grafana/issues/12668) * **Elasticsearch**: For alerting/backend, support having index name to the right of pattern in index pattern [#12731](https://github.com/grafana/grafana/issues/12731) -* **Auth**: Fix overriding tls_skip_verify_insecure using environment variable [#12747](https://github.com/grafana/grafana/issues/12747), thx [@jangaraj](https://github.com/jangaraj) +* **OAuth**: Fix overriding tls_skip_verify_insecure using environment variable [#12747](https://github.com/grafana/grafana/issues/12747), thx [@jangaraj](https://github.com/jangaraj) # 5.2.2 (2018-07-25) From 3d4a346c6621c6e685d338dc95aed0221c84c541 Mon Sep 17 00:00:00 2001 From: Tobias Skarhed Date: Mon, 30 Jul 2018 13:02:08 +0200 Subject: [PATCH 216/786] Begin conversion --- .../prometheus/specs/_datasource.jest.ts | 792 ++++++++++++++++++ 1 file changed, 792 insertions(+) create mode 100644 public/app/plugins/datasource/prometheus/specs/_datasource.jest.ts diff --git a/public/app/plugins/datasource/prometheus/specs/_datasource.jest.ts b/public/app/plugins/datasource/prometheus/specs/_datasource.jest.ts new file mode 100644 index 00000000000..384abc8f902 --- /dev/null +++ b/public/app/plugins/datasource/prometheus/specs/_datasource.jest.ts @@ -0,0 +1,792 @@ +import moment from 'moment'; +import { PrometheusDatasource } from '../datasource'; +import $q from 'q'; + +const SECOND = 1000; +const MINUTE = 60 * SECOND; +const HOUR = 60 * MINUTE; + +const time = ({ hours = 0, seconds = 0, minutes = 0 }) => moment(hours * HOUR + minutes * MINUTE + seconds * SECOND); + +let ctx = {}; +let instanceSettings = { + url: 'proxied', + directUrl: 'direct', + user: 'test', + password: 'mupp', + jsonData: { httpMethod: 'GET' }, +}; +let backendSrv = { + datasourceRequest: jest.fn(), +}; + +let templateSrv = { + replace: (target, scopedVars, format) => { + if (!target) { + return target; + } + let variable, value, fmt; + + return target.replace(scopedVars, (match, var1, var2, fmt2, var3, fmt3) => { + variable = this.index[var1 || var2 || var3]; + fmt = fmt2 || fmt3 || format; + if (scopedVars) { + value = scopedVars[var1 || var2 || var3]; + if (value) { + return this.formatValue(value.value, fmt, variable); + } + } + }); + }, +}; + +let timeSrv = { + timeRange: () => { + return { to: { diff: () => 2000 }, from: '' }; + }, +}; + +describe('PrometheusDatasource', function() { + // beforeEach(angularMocks.module('grafana.core')); + // beforeEach(angularMocks.module('grafana.services')); + // beforeEach(ctx.providePhase(['timeSrv'])); + + // beforeEach( + // angularMocks.inject(function($q, $rootScope, $httpBackend, $injector) { + // ctx.$q = $q; + // ctx.$httpBackend = $httpBackend; + // ctx.$rootScope = $rootScope; + // ctx.ds = $injector.instantiate(PrometheusDatasource, { + // instanceSettings: instanceSettings, + // }); + // $httpBackend.when('GET', /\.html$/).respond(''); + // }) + // ); + + beforeEach(() => { + ctx.ds = new PrometheusDatasource(instanceSettings, $q, backendSrv, templateSrv, timeSrv); + }); + describe('When querying prometheus with one target using query editor target spec', function() { + var results; + var query = { + range: { from: time({ seconds: 63 }), to: time({ seconds: 183 }) }, + targets: [{ expr: 'test{job="testjob"}', format: 'time_series' }], + interval: '60s', + }; + // Interval alignment with step + var urlExpected = + 'proxied/api/v1/query_range?query=' + encodeURIComponent('test{job="testjob"}') + '&start=60&end=240&step=60'; + var response = { + data: { + status: 'success', + data: { + resultType: 'matrix', + result: [ + { + metric: { __name__: 'test', job: 'testjob' }, + values: [[60, '3846']], + }, + ], + }, + }, + }; + beforeEach(async () => { + // ctx.$httpBackend.expect('GET', urlExpected).respond(response); + backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response)); + ctx.ds = new PrometheusDatasource(instanceSettings, $q, backendSrv, templateSrv, timeSrv); + + await ctx.ds.query(query).then(function(data) { + results = data; + }); + // ctx.$httpBackend.flush(); + }); + it('should generate the correct query', function() { + // ctx.$httpBackend.verifyNoOutstandingExpectation(); + let res = backendSrv.datasourceRequest.mock.calls[0][0]; + expect(res.method).toBe('GET'); + expect(res.url).toBe(urlExpected); + }); + it('should return series list', function() { + expect(results.data.length).toBe(1); + expect(results.data[0].target).toBe('test{job="testjob"}'); + }); + }); + describe('When querying prometheus with one target which return multiple series', function() { + var results; + var start = 60; + var end = 360; + var step = 60; + // var urlExpected = + // 'proxied/api/v1/query_range?query=' + + // encodeURIComponent('test{job="testjob"}') + + // '&start=' + + // start + + // '&end=' + + // end + + // '&step=' + + // step; + var query = { + range: { from: time({ seconds: start }), to: time({ seconds: end }) }, + targets: [{ expr: 'test{job="testjob"}', format: 'time_series' }], + interval: '60s', + }; + var response = { + status: 'success', + data: { + data: { + resultType: 'matrix', + result: [ + { + metric: { __name__: 'test', job: 'testjob', series: 'series 1' }, + values: [[start + step * 1, '3846'], [start + step * 3, '3847'], [end - step * 1, '3848']], + }, + { + metric: { __name__: 'test', job: 'testjob', series: 'series 2' }, + values: [[start + step * 2, '4846']], + }, + ], + }, + }, + }; + beforeEach(async () => { + backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response)); + ctx.ds = new PrometheusDatasource(instanceSettings, $q, backendSrv, templateSrv, timeSrv); + + await ctx.ds.query(query).then(function(data) { + results = data; + }); + }); + it('should be same length', function() { + expect(results.data.length).toBe(2); + expect(results.data[0].datapoints.length).toBe((end - start) / step + 1); + expect(results.data[1].datapoints.length).toBe((end - start) / step + 1); + }); + it('should fill null until first datapoint in response', function() { + expect(results.data[0].datapoints[0][1]).toBe(start * 1000); + expect(results.data[0].datapoints[0][0]).toBe(null); + expect(results.data[0].datapoints[1][1]).toBe((start + step * 1) * 1000); + expect(results.data[0].datapoints[1][0]).toBe(3846); + }); + it('should fill null after last datapoint in response', function() { + var length = (end - start) / step + 1; + expect(results.data[0].datapoints[length - 2][1]).toBe((end - step * 1) * 1000); + expect(results.data[0].datapoints[length - 2][0]).toBe(3848); + expect(results.data[0].datapoints[length - 1][1]).toBe(end * 1000); + expect(results.data[0].datapoints[length - 1][0]).toBe(null); + }); + it('should fill null at gap between series', function() { + expect(results.data[0].datapoints[2][1]).toBe((start + step * 2) * 1000); + expect(results.data[0].datapoints[2][0]).toBe(null); + expect(results.data[1].datapoints[1][1]).toBe((start + step * 1) * 1000); + expect(results.data[1].datapoints[1][0]).toBe(null); + expect(results.data[1].datapoints[3][1]).toBe((start + step * 3) * 1000); + expect(results.data[1].datapoints[3][0]).toBe(null); + }); + }); + describe('When querying prometheus with one target and instant = true', function() { + var results; + var urlExpected = 'proxied/api/v1/query?query=' + encodeURIComponent('test{job="testjob"}') + '&time=123'; + var query = { + range: { from: time({ seconds: 63 }), to: time({ seconds: 123 }) }, + targets: [{ expr: 'test{job="testjob"}', format: 'time_series', instant: true }], + interval: '60s', + }; + var response = { + status: 'success', + data: { + data: { + resultType: 'vector', + result: [ + { + metric: { __name__: 'test', job: 'testjob' }, + value: [123, '3846'], + }, + ], + }, + }, + }; + beforeEach(async () => { + backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response)); + ctx.ds = new PrometheusDatasource(instanceSettings, $q, backendSrv, templateSrv, timeSrv); + + await ctx.ds.query(query).then(function(data) { + results = data; + }); + }); + it('should generate the correct query', function() { + let res = backendSrv.datasourceRequest.mock.calls[0][0]; + expect(res.method).toBe('GET'); + expect(res.url).toBe(urlExpected); + }); + it('should return series list', function() { + expect(results.data.length).toBe(1); + expect(results.data[0].target).toBe('test{job="testjob"}'); + }); + }); + describe('When performing annotationQuery', function() { + var results; + // var urlExpected = + // 'proxied/api/v1/query_range?query=' + + // encodeURIComponent('ALERTS{alertstate="firing"}') + + // '&start=60&end=180&step=60'; + var options = { + annotation: { + expr: 'ALERTS{alertstate="firing"}', + tagKeys: 'job', + titleFormat: '{{alertname}}', + textFormat: '{{instance}}', + }, + range: { + from: time({ seconds: 63 }), + to: time({ seconds: 123 }), + }, + }; + var response = { + status: 'success', + data: { + data: { + resultType: 'matrix', + result: [ + { + metric: { + __name__: 'ALERTS', + alertname: 'InstanceDown', + alertstate: 'firing', + instance: 'testinstance', + job: 'testjob', + }, + values: [[123, '1']], + }, + ], + }, + }, + }; + beforeEach(async () => { + backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response)); + ctx.ds = new PrometheusDatasource(instanceSettings, $q, backendSrv, templateSrv, timeSrv); + + await ctx.ds.annotationQuery(options).then(function(data) { + results = data; + }); + }); + it('should return annotation list', function() { + // ctx.$rootScope.$apply(); + expect(results.length).toBe(1); + expect(results[0].tags).toContain('testjob'); + expect(results[0].title).toBe('InstanceDown'); + expect(results[0].text).toBe('testinstance'); + expect(results[0].time).toBe(123 * 1000); + }); + }); + + describe('When resultFormat is table and instant = true', function() { + var results; + var urlExpected = 'proxied/api/v1/query?query=' + encodeURIComponent('test{job="testjob"}') + '&time=123'; + var query = { + range: { from: time({ seconds: 63 }), to: time({ seconds: 123 }) }, + targets: [{ expr: 'test{job="testjob"}', format: 'time_series', instant: true }], + interval: '60s', + }; + var response = { + status: 'success', + data: { + data: { + resultType: 'vector', + result: [ + { + metric: { __name__: 'test', job: 'testjob' }, + value: [123, '3846'], + }, + ], + }, + }, + }; + + beforeEach(async () => { + backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response)); + ctx.ds = new PrometheusDatasource(instanceSettings, $q, backendSrv, templateSrv, timeSrv); + await ctx.ds.query(query).then(function(data) { + results = data; + }); + }); + + it('should return result', () => { + expect(results).not.toBe(null); + }); + }); + + describe('The "step" query parameter', function() { + var response = { + status: 'success', + data: { + data: { + resultType: 'matrix', + result: [], + }, + }, + }; + + it('should be min interval when greater than auto interval', async () => { + let query = { + // 6 minute range + range: { from: time({ minutes: 1 }), to: time({ minutes: 7 }) }, + targets: [ + { + expr: 'test', + interval: '10s', + }, + ], + interval: '5s', + }; + let urlExpected = 'proxied/api/v1/query_range?query=test&start=60&end=420&step=10'; + + backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response)); + ctx.ds = new PrometheusDatasource(instanceSettings, $q, backendSrv, templateSrv, timeSrv); + await ctx.ds.query(query); + let res = backendSrv.datasourceRequest.mock.calls[0][0]; + expect(res.method).toBe('GET'); + expect(res.url).toBe(urlExpected); + }); + + it('step should never go below 1', async () => { + var query = { + // 6 minute range + range: { from: time({ minutes: 1 }), to: time({ minutes: 7 }) }, + targets: [{ expr: 'test' }], + interval: '100ms', + }; + var urlExpected = 'proxied/api/v1/query_range?query=test&start=60&end=420&step=1'; + backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response)); + ctx.ds = new PrometheusDatasource(instanceSettings, $q, backendSrv, templateSrv, timeSrv); + await ctx.ds.query(query); + let res = backendSrv.datasourceRequest.mock.calls[0][0]; + expect(res.method).toBe('GET'); + expect(res.url).toBe(urlExpected); + }); + + it('should be auto interval when greater than min interval', async () => { + var query = { + // 6 minute range + range: { from: time({ minutes: 1 }), to: time({ minutes: 7 }) }, + targets: [ + { + expr: 'test', + interval: '5s', + }, + ], + interval: '10s', + }; + var urlExpected = 'proxied/api/v1/query_range?query=test&start=60&end=420&step=10'; + backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response)); + ctx.ds = new PrometheusDatasource(instanceSettings, $q, backendSrv, templateSrv, timeSrv); + await ctx.ds.query(query); + let res = backendSrv.datasourceRequest.mock.calls[0][0]; + expect(res.method).toBe('GET'); + expect(res.url).toBe(urlExpected); + }); + it('should result in querying fewer than 11000 data points', async () => { + var query = { + // 6 hour range + range: { from: time({ hours: 1 }), to: time({ hours: 7 }) }, + targets: [{ expr: 'test' }], + interval: '1s', + }; + var end = 7 * 60 * 60; + var start = 60 * 60; + var urlExpected = 'proxied/api/v1/query_range?query=test&start=' + start + '&end=' + end + '&step=2'; + backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response)); + ctx.ds = new PrometheusDatasource(instanceSettings, $q, backendSrv, templateSrv, timeSrv); + await ctx.ds.query(query); + let res = backendSrv.datasourceRequest.mock.calls[0][0]; + expect(res.method).toBe('GET'); + expect(res.url).toBe(urlExpected); + }); + it('should not apply min interval when interval * intervalFactor greater', async () => { + var query = { + // 6 minute range + range: { from: time({ minutes: 1 }), to: time({ minutes: 7 }) }, + targets: [ + { + expr: 'test', + interval: '10s', + intervalFactor: 10, + }, + ], + interval: '5s', + }; + // times get rounded up to interval + var urlExpected = 'proxied/api/v1/query_range?query=test&start=50&end=450&step=50'; + backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response)); + ctx.ds = new PrometheusDatasource(instanceSettings, $q, backendSrv, templateSrv, timeSrv); + await ctx.ds.query(query); + let res = backendSrv.datasourceRequest.mock.calls[0][0]; + expect(res.method).toBe('GET'); + expect(res.url).toBe(urlExpected); + }); + it('should apply min interval when interval * intervalFactor smaller', async () => { + var query = { + // 6 minute range + range: { from: time({ minutes: 1 }), to: time({ minutes: 7 }) }, + targets: [ + { + expr: 'test', + interval: '15s', + intervalFactor: 2, + }, + ], + interval: '5s', + }; + var urlExpected = 'proxied/api/v1/query_range?query=test' + '&start=60&end=420&step=15'; + backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response)); + ctx.ds = new PrometheusDatasource(instanceSettings, $q, backendSrv, templateSrv, timeSrv); + await ctx.ds.query(query); + let res = backendSrv.datasourceRequest.mock.calls[0][0]; + expect(res.method).toBe('GET'); + expect(res.url).toBe(urlExpected); + }); + it('should apply intervalFactor to auto interval when greater', async () => { + var query = { + // 6 minute range + range: { from: time({ minutes: 1 }), to: time({ minutes: 7 }) }, + targets: [ + { + expr: 'test', + interval: '5s', + intervalFactor: 10, + }, + ], + interval: '10s', + }; + // times get aligned to interval + var urlExpected = 'proxied/api/v1/query_range?query=test' + '&start=0&end=500&step=100'; + backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response)); + ctx.ds = new PrometheusDatasource(instanceSettings, $q, backendSrv, templateSrv, timeSrv); + await ctx.ds.query(query); + let res = backendSrv.datasourceRequest.mock.calls[0][0]; + expect(res.method).toBe('GET'); + expect(res.url).toBe(urlExpected); + }); + it('should not not be affected by the 11000 data points limit when large enough', async () => { + var query = { + // 1 week range + range: { from: time({}), to: time({ hours: 7 * 24 }) }, + targets: [ + { + expr: 'test', + intervalFactor: 10, + }, + ], + interval: '10s', + }; + var end = 7 * 24 * 60 * 60; + var start = 0; + var urlExpected = 'proxied/api/v1/query_range?query=test' + '&start=' + start + '&end=' + end + '&step=100'; + backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response)); + ctx.ds = new PrometheusDatasource(instanceSettings, $q, backendSrv, templateSrv, timeSrv); + await ctx.ds.query(query); + let res = backendSrv.datasourceRequest.mock.calls[0][0]; + expect(res.method).toBe('GET'); + expect(res.url).toBe(urlExpected); + }); + it('should be determined by the 11000 data points limit when too small', async () => { + var query = { + // 1 week range + range: { from: time({}), to: time({ hours: 7 * 24 }) }, + targets: [ + { + expr: 'test', + intervalFactor: 10, + }, + ], + interval: '5s', + }; + var end = 7 * 24 * 60 * 60; + var start = 0; + var urlExpected = 'proxied/api/v1/query_range?query=test' + '&start=' + start + '&end=' + end + '&step=60'; + backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response)); + ctx.ds = new PrometheusDatasource(instanceSettings, $q, backendSrv, templateSrv, timeSrv); + await ctx.ds.query(query); + let res = backendSrv.datasourceRequest.mock.calls[0][0]; + expect(res.method).toBe('GET'); + expect(res.url).toBe(urlExpected); + }); + }); + + describe('The __interval and __interval_ms template variables', function() { + var response = { + status: 'success', + data: { + data: { + resultType: 'matrix', + result: [], + }, + }, + }; + + it('should be unchanged when auto interval is greater than min interval', async () => { + var query = { + // 6 minute range + range: { from: time({ minutes: 1 }), to: time({ minutes: 7 }) }, + targets: [ + { + expr: 'rate(test[$__interval])', + interval: '5s', + }, + ], + interval: '10s', + scopedVars: { + __interval: { text: '10s', value: '10s' }, + __interval_ms: { text: 10 * 1000, value: 10 * 1000 }, + }, + }; + var urlExpected = + 'proxied/api/v1/query_range?query=' + encodeURIComponent('rate(test[10s])') + '&start=60&end=420&step=10'; + + backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response)); + ctx.ds = new PrometheusDatasource(instanceSettings, $q, backendSrv, templateSrv, timeSrv); + await ctx.ds.query(query); + let res = backendSrv.datasourceRequest.mock.calls[0][0]; + expect(res.method).toBe('GET'); + expect(res.url).toBe(urlExpected); + + expect(query.scopedVars.__interval.text).toBe('10s'); + expect(query.scopedVars.__interval.value).toBe('10s'); + expect(query.scopedVars.__interval_ms.text).toBe(10 * 1000); + expect(query.scopedVars.__interval_ms.value).toBe(10 * 1000); + }); + it('should be min interval when it is greater than auto interval', async () => { + var query = { + // 6 minute range + range: { from: time({ minutes: 1 }), to: time({ minutes: 7 }) }, + targets: [ + { + expr: 'rate(test[$__interval])', + interval: '10s', + }, + ], + interval: '5s', + scopedVars: { + __interval: { text: '5s', value: '5s' }, + __interval_ms: { text: 5 * 1000, value: 5 * 1000 }, + }, + }; + var urlExpected = + 'proxied/api/v1/query_range?query=' + encodeURIComponent('rate(test[10s])') + '&start=60&end=420&step=10'; + backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response)); + ctx.ds = new PrometheusDatasource(instanceSettings, $q, backendSrv, templateSrv, timeSrv); + await ctx.ds.query(query); + let res = backendSrv.datasourceRequest.mock.calls[0][0]; + expect(res.method).toBe('GET'); + expect(res.url).toBe(urlExpected); + + expect(query.scopedVars.__interval.text).toBe('5s'); + expect(query.scopedVars.__interval.value).toBe('5s'); + expect(query.scopedVars.__interval_ms.text).toBe(5 * 1000); + expect(query.scopedVars.__interval_ms.value).toBe(5 * 1000); + }); + it('should account for intervalFactor', async () => { + var query = { + // 6 minute range + range: { from: time({ minutes: 1 }), to: time({ minutes: 7 }) }, + targets: [ + { + expr: 'rate(test[$__interval])', + interval: '5s', + intervalFactor: 10, + }, + ], + interval: '10s', + scopedVars: { + __interval: { text: '10s', value: '10s' }, + __interval_ms: { text: 10 * 1000, value: 10 * 1000 }, + }, + }; + var urlExpected = + 'proxied/api/v1/query_range?query=' + encodeURIComponent('rate(test[100s])') + '&start=0&end=500&step=100'; + backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response)); + ctx.ds = new PrometheusDatasource(instanceSettings, $q, backendSrv, templateSrv, timeSrv); + await ctx.ds.query(query); + let res = backendSrv.datasourceRequest.mock.calls[0][0]; + expect(res.method).toBe('GET'); + expect(res.url).toBe(urlExpected); + + expect(query.scopedVars.__interval.text).toBe('10s'); + expect(query.scopedVars.__interval.value).toBe('10s'); + expect(query.scopedVars.__interval_ms.text).toBe(10 * 1000); + expect(query.scopedVars.__interval_ms.value).toBe(10 * 1000); + }); + it('should be interval * intervalFactor when greater than min interval', async () => { + var query = { + // 6 minute range + range: { from: time({ minutes: 1 }), to: time({ minutes: 7 }) }, + targets: [ + { + expr: 'rate(test[$__interval])', + interval: '10s', + intervalFactor: 10, + }, + ], + interval: '5s', + scopedVars: { + __interval: { text: '5s', value: '5s' }, + __interval_ms: { text: 5 * 1000, value: 5 * 1000 }, + }, + }; + var urlExpected = + 'proxied/api/v1/query_range?query=' + encodeURIComponent('rate(test[50s])') + '&start=50&end=450&step=50'; + backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response)); + ctx.ds = new PrometheusDatasource(instanceSettings, $q, backendSrv, templateSrv, timeSrv); + await ctx.ds.query(query); + let res = backendSrv.datasourceRequest.mock.calls[0][0]; + expect(res.method).toBe('GET'); + expect(res.url).toBe(urlExpected); + + expect(query.scopedVars.__interval.text).toBe('5s'); + expect(query.scopedVars.__interval.value).toBe('5s'); + expect(query.scopedVars.__interval_ms.text).toBe(5 * 1000); + expect(query.scopedVars.__interval_ms.value).toBe(5 * 1000); + }); + it('should be min interval when greater than interval * intervalFactor', async () => { + var query = { + // 6 minute range + range: { from: time({ minutes: 1 }), to: time({ minutes: 7 }) }, + targets: [ + { + expr: 'rate(test[$__interval])', + interval: '15s', + intervalFactor: 2, + }, + ], + interval: '5s', + scopedVars: { + __interval: { text: '5s', value: '5s' }, + __interval_ms: { text: 5 * 1000, value: 5 * 1000 }, + }, + }; + var urlExpected = + 'proxied/api/v1/query_range?query=' + encodeURIComponent('rate(test[15s])') + '&start=60&end=420&step=15'; + + backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response)); + ctx.ds = new PrometheusDatasource(instanceSettings, $q, backendSrv, templateSrv, timeSrv); + await ctx.ds.query(query); + let res = backendSrv.datasourceRequest.mock.calls[0][0]; + expect(res.method).toBe('GET'); + expect(res.url).toBe(urlExpected); + + expect(query.scopedVars.__interval.text).toBe('5s'); + expect(query.scopedVars.__interval.value).toBe('5s'); + expect(query.scopedVars.__interval_ms.text).toBe(5 * 1000); + expect(query.scopedVars.__interval_ms.value).toBe(5 * 1000); + }); + it('should be determined by the 11000 data points limit, accounting for intervalFactor', async () => { + var query = { + // 1 week range + range: { from: time({}), to: time({ hours: 7 * 24 }) }, + targets: [ + { + expr: 'rate(test[$__interval])', + intervalFactor: 10, + }, + ], + interval: '5s', + scopedVars: { + __interval: { text: '5s', value: '5s' }, + __interval_ms: { text: 5 * 1000, value: 5 * 1000 }, + }, + }; + var end = 7 * 24 * 60 * 60; + var start = 0; + var urlExpected = + 'proxied/api/v1/query_range?query=' + + encodeURIComponent('rate(test[60s])') + + '&start=' + + start + + '&end=' + + end + + '&step=60'; + backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response)); + ctx.ds = new PrometheusDatasource(instanceSettings, $q, backendSrv, templateSrv, timeSrv); + await ctx.ds.query(query); + let res = backendSrv.datasourceRequest.mock.calls[0][0]; + expect(res.method).toBe('GET'); + expect(res.url).toBe(urlExpected); + + expect(query.scopedVars.__interval.text).toBe('5s'); + expect(query.scopedVars.__interval.value).toBe('5s'); + expect(query.scopedVars.__interval_ms.text).toBe(5 * 1000); + expect(query.scopedVars.__interval_ms.value).toBe(5 * 1000); + }); + }); +}); + +describe('PrometheusDatasource for POST', function() { + // var ctx = new helpers.ServiceTestContext(); + let instanceSettings = { + url: 'proxied', + directUrl: 'direct', + user: 'test', + password: 'mupp', + jsonData: { httpMethod: 'POST' }, + }; + + // beforeEach(angularMocks.module('grafana.core')); + // beforeEach(angularMocks.module('grafana.services')); + // beforeEach(ctx.providePhase(['timeSrv'])); + + // beforeEach( + // // angularMocks.inject(function($q, $rootScope, $httpBackend, $injector) { + // // ctx.$q = $q; + // // ctx.$httpBackend = $httpBackend; + // // ctx.$rootScope = $rootScope; + // // ctx.ds = $injector.instantiate(PrometheusDatasource, { instanceSettings: instanceSettings }); + // // $httpBackend.when('GET', /\.html$/).respond(''); + // // }) + // ); + + describe('When querying prometheus with one target using query editor target spec', function() { + var results; + var urlExpected = 'proxied/api/v1/query_range'; + var dataExpected = { + query: 'test{job="testjob"}', + start: 1 * 60, + end: 3 * 60, + step: 60, + }; + var query = { + range: { from: time({ minutes: 1, seconds: 3 }), to: time({ minutes: 2, seconds: 3 }) }, + targets: [{ expr: 'test{job="testjob"}', format: 'time_series' }], + interval: '60s', + }; + var response = { + status: 'success', + data: { + data: { + resultType: 'matrix', + result: [ + { + metric: { __name__: 'test', job: 'testjob' }, + values: [[2 * 60, '3846']], + }, + ], + }, + }, + }; + beforeEach(async () => { + backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response)); + ctx.ds = new PrometheusDatasource(instanceSettings, $q, backendSrv, templateSrv, timeSrv); + await ctx.ds.query(query).then(function(data) { + results = data; + }); + }); + it('should generate the correct query', function() { + let res = backendSrv.datasourceRequest.mock.calls[0][0]; + expect(res.method).toBe('POST'); + expect(res.url).toBe(urlExpected); + expect(res.data).toEqual(dataExpected); + }); + it('should return series list', function() { + expect(results.data.length).toBe(1); + expect(results.data[0].target).toBe('test{job="testjob"}'); + }); + }); +}); From e32cf75c2d3caca0d62e3296701d63c9135e2233 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Mon, 30 Jul 2018 13:50:18 +0200 Subject: [PATCH 217/786] fix usage of metric column types so that you don't need to specify metric alias --- pkg/tsdb/sql_engine.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/pkg/tsdb/sql_engine.go b/pkg/tsdb/sql_engine.go index 027f37fc243..29428971c64 100644 --- a/pkg/tsdb/sql_engine.go +++ b/pkg/tsdb/sql_engine.go @@ -75,6 +75,10 @@ var NewSqlQueryEndpoint = func(config *SqlQueryEndpointConfiguration, rowTransfo queryEndpoint.timeColumnNames = config.TimeColumnNames } + if len(config.MetricColumnTypes) > 0 { + queryEndpoint.metricColumnTypes = config.MetricColumnTypes + } + engineCache.Lock() defer engineCache.Unlock() @@ -249,6 +253,7 @@ func (e *sqlQueryEndpoint) transformToTimeSeries(query *Query, rows *core.Rows, columnType := columnTypes[i].DatabaseTypeName() for _, mct := range e.metricColumnTypes { + e.log.Info(mct) if columnType == mct { metricIndex = i continue From 38a52c2489853eaff1ce036b864f736c59c9ba49 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Mon, 30 Jul 2018 13:50:52 +0200 Subject: [PATCH 218/786] mssql: update tests --- pkg/tsdb/mssql/mssql_test.go | 54 ++++++++++-------------------------- 1 file changed, 15 insertions(+), 39 deletions(-) diff --git a/pkg/tsdb/mssql/mssql_test.go b/pkg/tsdb/mssql/mssql_test.go index 8e3d617ca09..30d1da3bda1 100644 --- a/pkg/tsdb/mssql/mssql_test.go +++ b/pkg/tsdb/mssql/mssql_test.go @@ -615,7 +615,7 @@ func TestMSSQL(t *testing.T) { Queries: []*tsdb.Query{ { Model: simplejson.NewFromAny(map[string]interface{}{ - "rawSql": "SELECT $__timeEpoch(time), measurement AS metric, valueOne, valueTwo FROM metric_values ORDER BY 1", + "rawSql": "SELECT $__timeEpoch(time), measurement, valueOne, valueTwo FROM metric_values ORDER BY 1", "format": "time_series", }), RefId: "A", @@ -660,21 +660,9 @@ func TestMSSQL(t *testing.T) { SELECT CAST(ROUND(DATEDIFF(second, '1970-01-01', time)/CAST(@dInterval as float), 0) as bigint)*@dInterval as time, - measurement + ' - value one' as metric, - avg(valueOne) as value - FROM - metric_values - WHERE - time BETWEEN DATEADD(s, @from, '1970-01-01') AND DATEADD(s, @to, '1970-01-01') AND - (@metric = 'ALL' OR measurement = @metric) - GROUP BY - CAST(ROUND(DATEDIFF(second, '1970-01-01', time)/CAST(@dInterval as float), 0) as bigint)*@dInterval, - measurement - UNION ALL - SELECT - CAST(ROUND(DATEDIFF(second, '1970-01-01', time)/CAST(@dInterval as float), 0) as bigint)*@dInterval as time, - measurement + ' - value two' as metric, - avg(valueTwo) as value + measurement as metric, + avg(valueOne) as valueOne, + avg(valueTwo) as valueTwo FROM metric_values WHERE @@ -717,10 +705,10 @@ func TestMSSQL(t *testing.T) { So(queryResult.Error, ShouldBeNil) So(len(queryResult.Series), ShouldEqual, 4) - So(queryResult.Series[0].Name, ShouldEqual, "Metric A - value one") - So(queryResult.Series[1].Name, ShouldEqual, "Metric B - value one") - So(queryResult.Series[2].Name, ShouldEqual, "Metric A - value two") - So(queryResult.Series[3].Name, ShouldEqual, "Metric B - value two") + So(queryResult.Series[0].Name, ShouldEqual, "Metric A valueOne") + So(queryResult.Series[1].Name, ShouldEqual, "Metric A valueTwo") + So(queryResult.Series[2].Name, ShouldEqual, "Metric B valueOne") + So(queryResult.Series[3].Name, ShouldEqual, "Metric B valueTwo") }) }) @@ -749,21 +737,9 @@ func TestMSSQL(t *testing.T) { SELECT CAST(ROUND(DATEDIFF(second, '1970-01-01', time)/CAST(@dInterval as float), 0) as bigint)*@dInterval as time, - measurement + ' - value one' as metric, - avg(valueOne) as value - FROM - metric_values - WHERE - time BETWEEN @from AND @to AND - (@metric = 'ALL' OR measurement = @metric) - GROUP BY - CAST(ROUND(DATEDIFF(second, '1970-01-01', time)/CAST(@dInterval as float), 0) as bigint)*@dInterval, - measurement - UNION ALL - SELECT - CAST(ROUND(DATEDIFF(second, '1970-01-01', time)/CAST(@dInterval as float), 0) as bigint)*@dInterval as time, - measurement + ' - value two' as metric, - avg(valueTwo) as value + measurement as metric, + avg(valueOne) as valueOne, + avg(valueTwo) as valueTwo FROM metric_values WHERE @@ -806,10 +782,10 @@ func TestMSSQL(t *testing.T) { So(queryResult.Error, ShouldBeNil) So(len(queryResult.Series), ShouldEqual, 4) - So(queryResult.Series[0].Name, ShouldEqual, "Metric A - value one") - So(queryResult.Series[1].Name, ShouldEqual, "Metric B - value one") - So(queryResult.Series[2].Name, ShouldEqual, "Metric A - value two") - So(queryResult.Series[3].Name, ShouldEqual, "Metric B - value two") + So(queryResult.Series[0].Name, ShouldEqual, "Metric A valueOne") + So(queryResult.Series[1].Name, ShouldEqual, "Metric A valueTwo") + So(queryResult.Series[2].Name, ShouldEqual, "Metric B valueOne") + So(queryResult.Series[3].Name, ShouldEqual, "Metric B valueTwo") }) }) }) From 917b6b11b0fbae37d80a5dd097de031327e98679 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Mon, 30 Jul 2018 13:54:57 +0200 Subject: [PATCH 219/786] devenv: update sql dashboards --- .../datasource_tests_mssql_unittest.json | 73 ++++--------------- .../datasource_tests_mysql_unittest.json | 73 ++++--------------- .../datasource_tests_postgres_unittest.json | 73 ++++--------------- 3 files changed, 42 insertions(+), 177 deletions(-) diff --git a/devenv/dev-dashboards/datasource_tests_mssql_unittest.json b/devenv/dev-dashboards/datasource_tests_mssql_unittest.json index d47cfb0ad6e..0c7cc0fcc65 100644 --- a/devenv/dev-dashboards/datasource_tests_mssql_unittest.json +++ b/devenv/dev-dashboards/datasource_tests_mssql_unittest.json @@ -64,7 +64,7 @@ "editable": true, "gnetId": null, "graphTooltip": 0, - "iteration": 1532618879985, + "iteration": 1532949769359, "links": [], "panels": [ { @@ -871,14 +871,8 @@ { "alias": "", "format": "time_series", - "rawSql": "SELECT \n $__timeGroup(time, '$summarize') as time, \n measurement + ' - value one' as metric, \n avg(valueOne) as valueOne\nFROM\n metric_values \nWHERE\n $__timeFilter(time) AND\n ($metric = 'ALL' OR measurement = $metric)\nGROUP BY \n $__timeGroup(time, '$summarize'), \n measurement \nORDER BY 1", + "rawSql": "SELECT \n $__timeGroup(time, '$summarize') as time, \n measurement as metric, \n avg(valueOne) as valueOne,\n avg(valueTwo) as valueTwo\nFROM\n metric_values \nWHERE\n $__timeFilter(time) AND\n ($metric = 'ALL' OR measurement = $metric)\nGROUP BY \n $__timeGroup(time, '$summarize'), \n measurement \nORDER BY 1", "refId": "A" - }, - { - "alias": "", - "format": "time_series", - "rawSql": "SELECT \n $__timeGroup(time, '$summarize') as time, \n measurement + ' - value two' as metric, \n avg(valueTwo) as valueTwo \nFROM\n metric_values\nWHERE\n $__timeFilter(time) AND\n ($metric = 'ALL' OR measurement = $metric)\nGROUP BY \n $__timeGroup(time, '$summarize'), \n measurement \nORDER BY 1", - "refId": "B" } ], "thresholds": [], @@ -1067,14 +1061,8 @@ { "alias": "", "format": "time_series", - "rawSql": "SELECT $__timeEpoch(time), measurement + ' - value one' as metric, valueOne FROM metric_values WHERE $__timeFilter(time) AND ($metric = 'ALL' OR measurement = $metric) ORDER BY 1", + "rawSql": "SELECT $__timeEpoch(time), measurement as metric, valueOne, valueTwo FROM metric_values WHERE $__timeFilter(time) AND ($metric = 'ALL' OR measurement = $metric) ORDER BY 1", "refId": "A" - }, - { - "alias": "", - "format": "time_series", - "rawSql": "SELECT $__timeEpoch(time), measurement + ' - value two' as metric, valueTwo FROM metric_values WHERE $__timeFilter(time) AND ($metric = 'ALL' OR measurement = $metric) ORDER BY 1", - "refId": "B" } ], "thresholds": [], @@ -1245,14 +1233,8 @@ { "alias": "", "format": "time_series", - "rawSql": "SELECT $__timeEpoch(time), measurement + ' - value one' as metric, valueOne FROM metric_values WHERE $__timeFilter(time) AND ($metric = 'ALL' OR measurement = $metric) ORDER BY 1", + "rawSql": "SELECT $__timeEpoch(time), measurement as metric, valueOne, valueTwo FROM metric_values WHERE $__timeFilter(time) AND ($metric = 'ALL' OR measurement = $metric) ORDER BY 1", "refId": "A" - }, - { - "alias": "", - "format": "time_series", - "rawSql": "SELECT $__timeEpoch(time), measurement + ' - value two' as metric, valueTwo FROM metric_values WHERE $__timeFilter(time) AND ($metric = 'ALL' OR measurement = $metric) ORDER BY 1", - "refId": "B" } ], "thresholds": [], @@ -1423,14 +1405,8 @@ { "alias": "", "format": "time_series", - "rawSql": "SELECT $__timeEpoch(time), measurement + ' - value one' as metric, valueOne FROM metric_values WHERE $__timeFilter(time) AND ($metric = 'ALL' OR measurement = $metric) ORDER BY 1", + "rawSql": "SELECT $__timeEpoch(time), measurement as metric, valueOne, valueTwo FROM metric_values WHERE $__timeFilter(time) AND ($metric = 'ALL' OR measurement = $metric) ORDER BY 1", "refId": "A" - }, - { - "alias": "", - "format": "time_series", - "rawSql": "SELECT $__timeEpoch(time), measurement + ' - value two' as metric, valueTwo FROM metric_values WHERE $__timeFilter(time) AND ($metric = 'ALL' OR measurement = $metric) ORDER BY 1", - "refId": "B" } ], "thresholds": [], @@ -1773,14 +1749,8 @@ { "alias": "", "format": "time_series", - "rawSql": "SELECT $__timeEpoch(time), measurement + ' - value one' as metric, valueOne FROM metric_values \nWHERE $__timeFilter(time) AND ($metric = 'ALL' OR measurement = $metric) ORDER BY 1", + "rawSql": "SELECT $__timeEpoch(time), measurement as metric, valueOne, valueTwo FROM metric_values WHERE $__timeFilter(time) AND ($metric = 'ALL' OR measurement = $metric) ORDER BY 1", "refId": "A" - }, - { - "alias": "", - "format": "time_series", - "rawSql": "SELECT $__timeEpoch(time), measurement + ' - value two' as metric, valueTwo FROM metric_values \nWHERE $__timeFilter(time) AND ($metric = 'ALL' OR measurement = $metric) ORDER BY 1", - "refId": "B" } ], "thresholds": [], @@ -1954,14 +1924,8 @@ { "alias": "", "format": "time_series", - "rawSql": "SELECT $__timeEpoch(time), measurement + ' - value one' as metric, valueOne FROM metric_values \nWHERE $__timeFilter(time) AND ($metric = 'ALL' OR measurement = $metric) ORDER BY 1", + "rawSql": "SELECT $__timeEpoch(time), measurement as metric, valueOne, valueTwo FROM metric_values WHERE $__timeFilter(time) AND ($metric = 'ALL' OR measurement = $metric) ORDER BY 1", "refId": "A" - }, - { - "alias": "", - "format": "time_series", - "rawSql": "SELECT $__timeEpoch(time), measurement + ' - value two' as metric, valueTwo FROM metric_values \nWHERE $__timeFilter(time) AND ($metric = 'ALL' OR measurement = $metric) ORDER BY 1", - "refId": "B" } ], "thresholds": [], @@ -2135,14 +2099,8 @@ { "alias": "", "format": "time_series", - "rawSql": "SELECT $__timeEpoch(time), measurement + ' - value one' as metric, valueOne FROM metric_values \nWHERE $__timeFilter(time) AND ($metric = 'ALL' OR measurement = $metric) ORDER BY 1", + "rawSql": "SELECT $__timeEpoch(time), measurement as metric, valueOne, valueTwo FROM metric_values WHERE $__timeFilter(time) AND ($metric = 'ALL' OR measurement = $metric) ORDER BY 1", "refId": "A" - }, - { - "alias": "", - "format": "time_series", - "rawSql": "SELECT $__timeEpoch(time), measurement + ' - value two' as metric, valueTwo FROM metric_values \nWHERE $__timeFilter(time) AND ($metric = 'ALL' OR measurement = $metric) ORDER BY 1", - "refId": "B" } ], "thresholds": [], @@ -2316,14 +2274,8 @@ { "alias": "", "format": "time_series", - "rawSql": "SELECT $__timeEpoch(time), measurement + ' - value one' as metric, valueOne FROM metric_values\nWHERE $__timeFilter(time) AND ($metric = 'ALL' OR measurement = $metric) ORDER BY 1", + "rawSql": "SELECT $__timeEpoch(time), measurement as metric, valueOne, valueTwo FROM metric_values WHERE $__timeFilter(time) AND ($metric = 'ALL' OR measurement = $metric) ORDER BY 1", "refId": "A" - }, - { - "alias": "", - "format": "time_series", - "rawSql": "SELECT $__timeEpoch(time), measurement + ' - value two' as metric, valueTwo FROM metric_values \nWHERE $__timeFilter(time) AND ($metric = 'ALL' OR measurement = $metric) ORDER BY 1", - "refId": "B" } ], "thresholds": [], @@ -2460,7 +2412,10 @@ "refresh": false, "schemaVersion": 16, "style": "dark", - "tags": ["gdev", "mssql"], + "tags": [ + "gdev", + "mssql" + ], "templating": { "list": [ { @@ -2587,5 +2542,5 @@ "timezone": "", "title": "Datasource tests - MSSQL (unit test)", "uid": "GlAqcPgmz", - "version": 58 + "version": 3 } \ No newline at end of file diff --git a/devenv/dev-dashboards/datasource_tests_mysql_unittest.json b/devenv/dev-dashboards/datasource_tests_mysql_unittest.json index 326114ec8ff..e95eedf254c 100644 --- a/devenv/dev-dashboards/datasource_tests_mysql_unittest.json +++ b/devenv/dev-dashboards/datasource_tests_mysql_unittest.json @@ -64,7 +64,7 @@ "editable": true, "gnetId": null, "graphTooltip": 0, - "iteration": 1532620354037, + "iteration": 1532949531280, "links": [], "panels": [ { @@ -871,14 +871,8 @@ { "alias": "", "format": "time_series", - "rawSql": "SELECT \n $__timeGroup(time, '$summarize') as time, \n CONCAT(measurement, ' - value one') as metric, \n avg(valueOne) as valueOne\nFROM\n metric_values \nWHERE\n $__timeFilter(time) AND\n measurement IN($metric)\nGROUP BY 1, 2\nORDER BY 1", + "rawSql": "SELECT \n $__timeGroup(time, '$summarize') as time, \n measurement as metric, \n avg(valueOne) as valueOne,\n avg(valueTwo) as valueTwo\nFROM\n metric_values \nWHERE\n $__timeFilter(time) AND\n measurement IN($metric)\nGROUP BY 1, 2\nORDER BY 1", "refId": "A" - }, - { - "alias": "", - "format": "time_series", - "rawSql": "SELECT \n $__timeGroup(time, '$summarize') as time, \n CONCAT(measurement, ' - value two') as metric, \n avg(valueTwo) as valueTwo \nFROM\n metric_values\nWHERE\n $__timeFilter(time) AND\n measurement IN($metric)\nGROUP BY 1,2\nORDER BY 1", - "refId": "B" } ], "thresholds": [], @@ -1061,14 +1055,8 @@ { "alias": "", "format": "time_series", - "rawSql": "SELECT $__time(time), CONCAT(measurement, ' - value one') as metric, valueOne FROM metric_values WHERE $__timeFilter(time) AND measurement IN($metric) ORDER BY 1", + "rawSql": "SELECT $__time(time), measurement as metric, valueOne, valueTwo FROM metric_values WHERE $__timeFilter(time) AND measurement IN($metric) ORDER BY 1", "refId": "A" - }, - { - "alias": "", - "format": "time_series", - "rawSql": "SELECT $__time(time), CONCAT(measurement, ' - value two') as metric, valueTwo FROM metric_values WHERE $__timeFilter(time) AND measurement IN($metric) ORDER BY 1", - "refId": "B" } ], "thresholds": [], @@ -1239,14 +1227,8 @@ { "alias": "", "format": "time_series", - "rawSql": "SELECT $__time(time), CONCAT(measurement, ' - value one') as metric, valueOne FROM metric_values WHERE $__timeFilter(time) AND measurement IN($metric) ORDER BY 1", + "rawSql": "SELECT $__time(time), measurement as metric, valueOne, valueTwo FROM metric_values WHERE $__timeFilter(time) AND measurement IN($metric) ORDER BY 1", "refId": "A" - }, - { - "alias": "", - "format": "time_series", - "rawSql": "SELECT $__time(time), CONCAT(measurement, ' - value two') as metric, valueTwo FROM metric_values WHERE $__timeFilter(time) AND measurement IN($metric) ORDER BY 1", - "refId": "B" } ], "thresholds": [], @@ -1417,14 +1399,8 @@ { "alias": "", "format": "time_series", - "rawSql": "SELECT $__time(time), CONCAT(measurement, ' - value one') as metric, valueOne FROM metric_values WHERE $__timeFilter(time) AND measurement IN($metric) ORDER BY 1", + "rawSql": "SELECT $__time(time), measurement as metric, valueOne, valueTwo FROM metric_values WHERE $__timeFilter(time) AND measurement IN($metric) ORDER BY 1", "refId": "A" - }, - { - "alias": "", - "format": "time_series", - "rawSql": "SELECT $__time(time), CONCAT(measurement, ' - value two') as metric, valueTwo FROM metric_values WHERE $__timeFilter(time) AND measurement IN($metric) ORDER BY 1", - "refId": "B" } ], "thresholds": [], @@ -1593,14 +1569,8 @@ { "alias": "", "format": "time_series", - "rawSql": "SELECT $__timeEpoch(time), CONCAT(measurement, ' - value one') as metric, valueOne FROM metric_values \nWHERE $__timeFilter(time) AND measurement IN($metric) ORDER BY 1", + "rawSql": "SELECT $__time(time), measurement as metric, valueOne, valueTwo FROM metric_values WHERE $__timeFilter(time) AND measurement IN($metric) ORDER BY 1", "refId": "A" - }, - { - "alias": "", - "format": "time_series", - "rawSql": "SELECT $__timeEpoch(time), CONCAT(measurement, ' - value two') as metric, valueTwo FROM metric_values \nWHERE $__timeFilter(time) AND measurement IN($metric) ORDER BY 1", - "refId": "B" } ], "thresholds": [], @@ -1774,14 +1744,8 @@ { "alias": "", "format": "time_series", - "rawSql": "SELECT $__timeEpoch(time), CONCAT(measurement, ' - value one') as metric, valueOne FROM metric_values \nWHERE $__timeFilter(time) AND measurement IN($metric) ORDER BY 1", + "rawSql": "SELECT $__time(time), measurement as metric, valueOne, valueTwo FROM metric_values WHERE $__timeFilter(time) AND measurement IN($metric) ORDER BY 1", "refId": "A" - }, - { - "alias": "", - "format": "time_series", - "rawSql": "SELECT $__timeEpoch(time), CONCAT(measurement, ' - value two') as metric, valueTwo FROM metric_values \nWHERE $__timeFilter(time) AND measurement IN($metric) ORDER BY 1", - "refId": "B" } ], "thresholds": [], @@ -1955,14 +1919,8 @@ { "alias": "", "format": "time_series", - "rawSql": "SELECT $__timeEpoch(time), CONCAT(measurement, ' - value one') as metric, valueOne FROM metric_values \nWHERE $__timeFilter(time) AND measurement IN($metric) ORDER BY 1", + "rawSql": "SELECT $__time(time), measurement as metric, valueOne, valueTwo FROM metric_values WHERE $__timeFilter(time) AND measurement IN($metric) ORDER BY 1", "refId": "A" - }, - { - "alias": "", - "format": "time_series", - "rawSql": "SELECT $__timeEpoch(time), CONCAT(measurement, ' - value two') as metric, valueTwo FROM metric_values \nWHERE $__timeFilter(time) AND measurement IN($metric) ORDER BY 1", - "refId": "B" } ], "thresholds": [], @@ -2136,14 +2094,8 @@ { "alias": "", "format": "time_series", - "rawSql": "SELECT $__timeEpoch(time), CONCAT(measurement, ' - value one') as metric, valueOne FROM metric_values \nWHERE $__timeFilter(time) AND measurement IN($metric) ORDER BY 1", + "rawSql": "SELECT $__time(time), measurement as metric, valueOne, valueTwo FROM metric_values WHERE $__timeFilter(time) AND measurement IN($metric) ORDER BY 1", "refId": "A" - }, - { - "alias": "", - "format": "time_series", - "rawSql": "SELECT $__timeEpoch(time), CONCAT(measurement, ' - value two') as metric, valueTwo FROM metric_values \nWHERE $__timeFilter(time) AND measurement IN($metric) ORDER BY 1", - "refId": "B" } ], "thresholds": [], @@ -2280,7 +2232,10 @@ "refresh": false, "schemaVersion": 16, "style": "dark", - "tags": ["gdev", "mysql"], + "tags": [ + "gdev", + "mysql" + ], "templating": { "list": [ { @@ -2405,5 +2360,5 @@ "timezone": "", "title": "Datasource tests - MySQL (unittest)", "uid": "Hmf8FDkmz", - "version": 12 + "version": 1 } \ No newline at end of file diff --git a/devenv/dev-dashboards/datasource_tests_postgres_unittest.json b/devenv/dev-dashboards/datasource_tests_postgres_unittest.json index 85151089b7f..2243baed0aa 100644 --- a/devenv/dev-dashboards/datasource_tests_postgres_unittest.json +++ b/devenv/dev-dashboards/datasource_tests_postgres_unittest.json @@ -64,7 +64,7 @@ "editable": true, "gnetId": null, "graphTooltip": 0, - "iteration": 1532619575136, + "iteration": 1532951521836, "links": [], "panels": [ { @@ -871,14 +871,8 @@ { "alias": "", "format": "time_series", - "rawSql": "SELECT \n $__timeGroup(time, '$summarize'), \n measurement || ' - value one' as metric, \n avg(\"valueOne\") as \"valueOne\"\nFROM\n metric_values \nWHERE\n $__timeFilter(time) AND\n measurement in($metric)\nGROUP BY 1, 2\nORDER BY 1", + "rawSql": "SELECT \n $__timeGroup(time, '$summarize'), \n measurement, \n avg(\"valueOne\") as \"valueOne\",\n avg(\"valueTwo\") as \"valueTwo\"\nFROM\n metric_values \nWHERE\n $__timeFilter(time) AND\n measurement in($metric)\nGROUP BY 1, 2\nORDER BY 1", "refId": "A" - }, - { - "alias": "", - "format": "time_series", - "rawSql": "SELECT \n $__timeGroup(time, '$summarize'), \n measurement || ' - value two' as metric, \n avg(\"valueTwo\") as \"valueTwo\"\nFROM\n metric_values \nWHERE\n $__timeFilter(time) AND\n measurement in($metric)\nGROUP BY 1, 2\nORDER BY 1", - "refId": "B" } ], "thresholds": [], @@ -1049,14 +1043,8 @@ { "alias": "", "format": "time_series", - "rawSql": "SELECT $__timeEpoch(time), measurement || ' - value one' as metric, \"valueOne\" FROM metric_values \nWHERE $__timeFilter(time) AND measurement in($metric) ORDER BY 1", + "rawSql": "SELECT $__timeEpoch(time), measurement, \"valueOne\", \"valueTwo\" FROM metric_values \nWHERE $__timeFilter(time) AND measurement in($metric) ORDER BY 1", "refId": "A" - }, - { - "alias": "", - "format": "time_series", - "rawSql": "SELECT $__timeEpoch(time), measurement || ' - value two' as metric, \"valueTwo\" FROM metric_values \nWHERE $__timeFilter(time) AND measurement in($metric) ORDER BY 1", - "refId": "B" } ], "thresholds": [], @@ -1227,14 +1215,8 @@ { "alias": "", "format": "time_series", - "rawSql": "SELECT $__timeEpoch(time), measurement || ' - value one' as metric, \"valueOne\" FROM metric_values \nWHERE $__timeFilter(time) AND measurement in($metric) ORDER BY 1", + "rawSql": "SELECT $__timeEpoch(time), measurement, \"valueOne\", \"valueTwo\" FROM metric_values \nWHERE $__timeFilter(time) AND measurement in($metric) ORDER BY 1", "refId": "A" - }, - { - "alias": "", - "format": "time_series", - "rawSql": "SELECT $__timeEpoch(time), measurement || ' - value two' as metric, \"valueTwo\" FROM metric_values \nWHERE $__timeFilter(time) AND measurement in($metric) ORDER BY 1", - "refId": "B" } ], "thresholds": [], @@ -1405,14 +1387,8 @@ { "alias": "", "format": "time_series", - "rawSql": "SELECT $__timeEpoch(time), measurement || ' - value one' as metric, \"valueOne\" FROM metric_values \nWHERE $__timeFilter(time) AND measurement in($metric) ORDER BY 1", + "rawSql": "SELECT $__timeEpoch(time), measurement, \"valueOne\", \"valueTwo\" FROM metric_values \nWHERE $__timeFilter(time) AND measurement in($metric) ORDER BY 1", "refId": "A" - }, - { - "alias": "", - "format": "time_series", - "rawSql": "SELECT $__timeEpoch(time), measurement || ' - value two' as metric, \"valueTwo\" FROM metric_values \nWHERE $__timeFilter(time) AND measurement in($metric) ORDER BY 1", - "refId": "B" } ], "thresholds": [], @@ -1581,14 +1557,8 @@ { "alias": "", "format": "time_series", - "rawSql": "SELECT $__timeEpoch(time), measurement || ' - value one' as metric, \"valueOne\" FROM metric_values \nWHERE $__timeFilter(time) AND measurement in($metric) ORDER BY 1", + "rawSql": "SELECT $__timeEpoch(time), measurement, \"valueOne\", \"valueTwo\" FROM metric_values \nWHERE $__timeFilter(time) AND measurement in($metric) ORDER BY 1", "refId": "A" - }, - { - "alias": "", - "format": "time_series", - "rawSql": "SELECT $__timeEpoch(time), measurement || ' - value two' as metric, \"valueTwo\" FROM metric_values \nWHERE $__timeFilter(time) AND measurement in($metric) ORDER BY 1", - "refId": "B" } ], "thresholds": [], @@ -1762,14 +1732,8 @@ { "alias": "", "format": "time_series", - "rawSql": "SELECT $__timeEpoch(time), measurement || ' - value one' as metric, \"valueOne\" FROM metric_values \nWHERE $__timeFilter(time) AND measurement in($metric) ORDER BY 1", + "rawSql": "SELECT $__timeEpoch(time), measurement, \"valueOne\", \"valueTwo\" FROM metric_values \nWHERE $__timeFilter(time) AND measurement in($metric) ORDER BY 1", "refId": "A" - }, - { - "alias": "", - "format": "time_series", - "rawSql": "SELECT $__timeEpoch(time), measurement || ' - value two' as metric, \"valueTwo\" FROM metric_values \nWHERE $__timeFilter(time) AND measurement in($metric) ORDER BY 1", - "refId": "B" } ], "thresholds": [], @@ -1943,14 +1907,8 @@ { "alias": "", "format": "time_series", - "rawSql": "SELECT $__timeEpoch(time), measurement || ' - value one' as metric, \"valueOne\" FROM metric_values \nWHERE $__timeFilter(time) AND measurement in($metric) ORDER BY 1", + "rawSql": "SELECT $__timeEpoch(time), measurement, \"valueOne\", \"valueTwo\" FROM metric_values \nWHERE $__timeFilter(time) AND measurement in($metric) ORDER BY 1", "refId": "A" - }, - { - "alias": "", - "format": "time_series", - "rawSql": "SELECT $__timeEpoch(time), measurement || ' - value two' as metric, \"valueTwo\" FROM metric_values \nWHERE $__timeFilter(time) AND measurement in($metric) ORDER BY 1", - "refId": "B" } ], "thresholds": [], @@ -2124,14 +2082,8 @@ { "alias": "", "format": "time_series", - "rawSql": "SELECT $__timeEpoch(time), measurement || ' - value one' as metric, \"valueOne\" FROM metric_values \nWHERE $__timeFilter(time) AND measurement in($metric) ORDER BY 1", + "rawSql": "SELECT $__timeEpoch(time), measurement, \"valueOne\", \"valueTwo\" FROM metric_values \nWHERE $__timeFilter(time) AND measurement in($metric) ORDER BY 1", "refId": "A" - }, - { - "alias": "", - "format": "time_series", - "rawSql": "SELECT $__timeEpoch(time), measurement || ' - value two' as metric, \"valueTwo\" FROM metric_values \nWHERE $__timeFilter(time) AND measurement in($metric) ORDER BY 1", - "refId": "B" } ], "thresholds": [], @@ -2268,7 +2220,10 @@ "refresh": false, "schemaVersion": 16, "style": "dark", - "tags": ["gdev", "postgres"], + "tags": [ + "gdev", + "postgres" + ], "templating": { "list": [ { @@ -2397,5 +2352,5 @@ "timezone": "", "title": "Datasource tests - Postgres (unittest)", "uid": "vHQdlVziz", - "version": 17 + "version": 1 } \ No newline at end of file From 8a22129177a8f3656cd55b411245d516a16c4c87 Mon Sep 17 00:00:00 2001 From: Sven Klemm Date: Mon, 30 Jul 2018 14:37:23 +0200 Subject: [PATCH 220/786] add version note to metric prefix and fix typo --- docs/sources/features/datasources/mssql.md | 3 ++- docs/sources/features/datasources/mysql.md | 3 ++- docs/sources/features/datasources/postgres.md | 3 ++- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/docs/sources/features/datasources/mssql.md b/docs/sources/features/datasources/mssql.md index bcb965dda74..ea7be8e1c30 100644 --- a/docs/sources/features/datasources/mssql.md +++ b/docs/sources/features/datasources/mssql.md @@ -148,7 +148,8 @@ The resulting table panel: ## Time series queries -If you set `Format as` to `Time series`, for use in Graph panel for example, then the query must must have a column named `time` that returns either a sql datetime or any numeric datatype representing unix epoch in seconds. You may return a column named `metric` that is used as metric name for the value column. Any column except `time` and `metric` is treated as a value column. If you omit the `metric` column, tha name of the value column will be the metric name. You may select multiple value columns, each will have its name as metric. If you return multiple value columns and a column named `metric` then this column is used as prefix for the series name. +If you set `Format as` to `Time series`, for use in Graph panel for example, then the query must must have a column named `time` that returns either a sql datetime or any numeric datatype representing unix epoch in seconds. You may return a column named `metric` that is used as metric name for the value column. Any column except `time` and `metric` is treated as a value column. If you omit the `metric` column, the name of the value column will be the metric name. You may select multiple value columns, each will have its name as metric. +If you return multiple value columns and a column named `metric` then this column is used as prefix for the series name (only available in Grafana 5.3+). **Example database table:** diff --git a/docs/sources/features/datasources/mysql.md b/docs/sources/features/datasources/mysql.md index c6e620eb08b..22287b2a838 100644 --- a/docs/sources/features/datasources/mysql.md +++ b/docs/sources/features/datasources/mysql.md @@ -103,7 +103,8 @@ The resulting table panel: If you set `Format as` to `Time series`, for use in Graph panel for example, then the query must return a column named `time` that returns either a sql datetime or any numeric datatype representing unix epoch. Any column except `time` and `metric` is treated as a value column. -You may return a column named `metric` that is used as metric name for the value column. If you return multiple value columns and a column named `metric` then this column is used as prefix for the series name. +You may return a column named `metric` that is used as metric name for the value column. +If you return multiple value columns and a column named `metric` then this column is used as prefix for the series name (only available in Grafana 5.3+). **Example with `metric` column:** diff --git a/docs/sources/features/datasources/postgres.md b/docs/sources/features/datasources/postgres.md index f3e52ed6652..793b3b6f4c0 100644 --- a/docs/sources/features/datasources/postgres.md +++ b/docs/sources/features/datasources/postgres.md @@ -101,7 +101,8 @@ The resulting table panel: If you set `Format as` to `Time series`, for use in Graph panel for example, then the query must return a column named `time` that returns either a sql datetime or any numeric datatype representing unix epoch. Any column except `time` and `metric` is treated as a value column. -You may return a column named `metric` that is used as metric name for the value column. If you return multiple value columns and a column named `metric` then this column is used as prefix for the series name. +You may return a column named `metric` that is used as metric name for the value column. +If you return multiple value columns and a column named `metric` then this column is used as prefix for the series name (only available in Grafana 5.3+). **Example with `metric` column:** From 9c0fbe5a0b3c2e334cff6d6bbe2cb4d5ae48a5fd Mon Sep 17 00:00:00 2001 From: Worty <6840978+Worty@users.noreply.github.com> Date: Mon, 30 Jul 2018 16:19:31 +0200 Subject: [PATCH 221/786] fixed that missing one --- public/app/core/utils/kbn.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/app/core/utils/kbn.ts b/public/app/core/utils/kbn.ts index 74ef2a9e874..7bf2cdc5fd6 100644 --- a/public/app/core/utils/kbn.ts +++ b/public/app/core/utils/kbn.ts @@ -1118,7 +1118,7 @@ kbn.getUnitFormats = function() { submenu: [ { text: 'parts-per-million (ppm)', value: 'ppm' }, { text: 'parts-per-billion (ppb)', value: 'conppb' }, - { text: 'nanogram per cubic metre (ng/m3)', value: 'conngm3' }, + { text: 'nanogram per cubic metre (ng/m³)', value: 'conngm3' }, { text: 'nanogram per normal cubic metre (ng/Nm³)', value: 'conngNm3' }, { text: 'microgram per cubic metre (μg/m³)', value: 'conμgm3' }, { text: 'microgram per normal cubic metre (μg/Nm³)', value: 'conμgNm3' }, From 4fa979649cf412c491a1d9d42d1d0062b13ff55d Mon Sep 17 00:00:00 2001 From: Worty <6840978+Worty@users.noreply.github.com> Date: Mon, 30 Jul 2018 16:28:19 +0200 Subject: [PATCH 222/786] also fixed "Watt per square metre" --- public/app/core/utils/kbn.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/public/app/core/utils/kbn.ts b/public/app/core/utils/kbn.ts index 7bf2cdc5fd6..c2764670b95 100644 --- a/public/app/core/utils/kbn.ts +++ b/public/app/core/utils/kbn.ts @@ -500,7 +500,7 @@ kbn.valueFormats.watt = kbn.formatBuilders.decimalSIPrefix('W'); kbn.valueFormats.kwatt = kbn.formatBuilders.decimalSIPrefix('W', 1); kbn.valueFormats.mwatt = kbn.formatBuilders.decimalSIPrefix('W', -1); kbn.valueFormats.kwattm = kbn.formatBuilders.decimalSIPrefix('W/Min', 1); -kbn.valueFormats.Wm2 = kbn.formatBuilders.fixedUnit('W/m2'); +kbn.valueFormats.Wm2 = kbn.formatBuilders.fixedUnit('W/m²'); kbn.valueFormats.voltamp = kbn.formatBuilders.decimalSIPrefix('VA'); kbn.valueFormats.kvoltamp = kbn.formatBuilders.decimalSIPrefix('VA', 1); kbn.valueFormats.voltampreact = kbn.formatBuilders.decimalSIPrefix('var'); @@ -1021,7 +1021,7 @@ kbn.getUnitFormats = function() { { text: 'Watt (W)', value: 'watt' }, { text: 'Kilowatt (kW)', value: 'kwatt' }, { text: 'Milliwatt (mW)', value: 'mwatt' }, - { text: 'Watt per square metre (W/m2)', value: 'Wm2' }, + { text: 'Watt per square metre (W/m²)', value: 'Wm2' }, { text: 'Volt-ampere (VA)', value: 'voltamp' }, { text: 'Kilovolt-ampere (kVA)', value: 'kvoltamp' }, { text: 'Volt-ampere reactive (var)', value: 'voltampreact' }, From 88d8072be3cd17ee7461481f1c17c51e69ed36b3 Mon Sep 17 00:00:00 2001 From: Jason Pereira Date: Mon, 30 Jul 2018 15:51:15 +0100 Subject: [PATCH 223/786] add aws_dx to cloudwatch datasource --- pkg/tsdb/cloudwatch/metric_find_query.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pkg/tsdb/cloudwatch/metric_find_query.go b/pkg/tsdb/cloudwatch/metric_find_query.go index 136ee241c2e..d2bd135ecc9 100644 --- a/pkg/tsdb/cloudwatch/metric_find_query.go +++ b/pkg/tsdb/cloudwatch/metric_find_query.go @@ -46,6 +46,7 @@ func init() { "AWS/CloudFront": {"Requests", "BytesDownloaded", "BytesUploaded", "TotalErrorRate", "4xxErrorRate", "5xxErrorRate"}, "AWS/CloudSearch": {"SuccessfulRequests", "SearchableDocuments", "IndexUtilization", "Partitions"}, "AWS/DMS": {"FreeableMemory", "WriteIOPS", "ReadIOPS", "WriteThroughput", "ReadThroughput", "WriteLatency", "ReadLatency", "SwapUsage", "NetworkTransmitThroughput", "NetworkReceiveThroughput", "FullLoadThroughputBandwidthSource", "FullLoadThroughputBandwidthTarget", "FullLoadThroughputRowsSource", "FullLoadThroughputRowsTarget", "CDCIncomingChanges", "CDCChangesMemorySource", "CDCChangesMemoryTarget", "CDCChangesDiskSource", "CDCChangesDiskTarget", "CDCThroughputBandwidthTarget", "CDCThroughputRowsSource", "CDCThroughputRowsTarget", "CDCLatencySource", "CDCLatencyTarget"}, + "AWS/DX": {"ConnectionState", "ConnectionBpsEgress", "ConnectionBpsIngress", "ConnectionPpsEgress", "ConnectionPpsIngress", "ConnectionCRCErrorCount", "ConnectionLightLevelTx", "ConnectionLightLevelRx"}, "AWS/DynamoDB": {"ConditionalCheckFailedRequests", "ConsumedReadCapacityUnits", "ConsumedWriteCapacityUnits", "OnlineIndexConsumedWriteCapacity", "OnlineIndexPercentageProgress", "OnlineIndexThrottleEvents", "ProvisionedReadCapacityUnits", "ProvisionedWriteCapacityUnits", "ReadThrottleEvents", "ReturnedBytes", "ReturnedItemCount", "ReturnedRecordsCount", "SuccessfulRequestLatency", "SystemErrors", "TimeToLiveDeletedItemCount", "ThrottledRequests", "UserErrors", "WriteThrottleEvents"}, "AWS/EBS": {"VolumeReadBytes", "VolumeWriteBytes", "VolumeReadOps", "VolumeWriteOps", "VolumeTotalReadTime", "VolumeTotalWriteTime", "VolumeIdleTime", "VolumeQueueLength", "VolumeThroughputPercentage", "VolumeConsumedReadWriteOps", "BurstBalance"}, "AWS/EC2": {"CPUCreditUsage", "CPUCreditBalance", "CPUUtilization", "DiskReadOps", "DiskWriteOps", "DiskReadBytes", "DiskWriteBytes", "NetworkIn", "NetworkOut", "NetworkPacketsIn", "NetworkPacketsOut", "StatusCheckFailed", "StatusCheckFailed_Instance", "StatusCheckFailed_System"}, @@ -118,6 +119,7 @@ func init() { "AWS/CloudFront": {"DistributionId", "Region"}, "AWS/CloudSearch": {}, "AWS/DMS": {"ReplicationInstanceIdentifier", "ReplicationTaskIdentifier"}, + "AWS/DX": {"ConnectionId"}, "AWS/DynamoDB": {"TableName", "GlobalSecondaryIndexName", "Operation", "StreamLabel"}, "AWS/EBS": {"VolumeId"}, "AWS/EC2": {"AutoScalingGroupName", "ImageId", "InstanceId", "InstanceType"}, From 162d3e8036f8365e294502b6dcd496518c951a5b Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Mon, 30 Jul 2018 17:03:01 +0200 Subject: [PATCH 224/786] changelog: add notes about closing #12727 [skip ci] --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b8f5bced972..c2e8c5c788e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ * **Prometheus**: Add $interval, $interval_ms, $range, and $range_ms support for dashboard and template queries [#12597](https://github.com/grafana/grafana/issues/12597) * **Variables**: Skip unneeded extra query request when de-selecting variable values used for repeated panels [#8186](https://github.com/grafana/grafana/issues/8186), thx [@mtanda](https://github.com/mtanda) * **Postgres/MySQL/MSSQL**: Use floor rounding in $__timeGroup macro function [#12460](https://github.com/grafana/grafana/issues/12460), thx [@svenklemm](https://github.com/svenklemm) +* **Postgres/MySQL/MSSQL**: Use metric column as prefix when returning multiple value columns [#12727](https://github.com/grafana/grafana/issues/12727), thx [@svenklemm](https://github.com/svenklemm) * **MySQL/MSSQL**: Use datetime format instead of epoch for $__timeFilter, $__timeFrom and $__timeTo macros [#11618](https://github.com/grafana/grafana/issues/11618) [#11619](https://github.com/grafana/grafana/issues/11619), thx [@AustinWinstanley](https://github.com/AustinWinstanley) * **Postgres**: Escape ssl mode parameter in connectionstring [#12644](https://github.com/grafana/grafana/issues/12644), thx [@yogyrahmawan](https://github.com/yogyrahmawan) * **Github OAuth**: Allow changes of user info at Github to be synched to Grafana when signing in [#11818](https://github.com/grafana/grafana/issues/11818), thx [@rwaweber](https://github.com/rwaweber) From ad84a145f56f1fc1a8d513014c05ef40326f89a4 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Mon, 30 Jul 2018 17:03:24 +0200 Subject: [PATCH 225/786] changelog: add notes about closing #12744 [skip ci] --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c2e8c5c788e..11baca97714 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,7 @@ * **Table**: Adjust header contrast for the light theme [#12668](https://github.com/grafana/grafana/issues/12668) * **Elasticsearch**: For alerting/backend, support having index name to the right of pattern in index pattern [#12731](https://github.com/grafana/grafana/issues/12731) * **OAuth**: Fix overriding tls_skip_verify_insecure using environment variable [#12747](https://github.com/grafana/grafana/issues/12747), thx [@jangaraj](https://github.com/jangaraj) +* **Units**: Change units to include characters for power of 2 and 3 [#12744](https://github.com/grafana/grafana/pull/12744), thx [@Worty](https://github.com/Worty) # 5.2.2 (2018-07-25) From e4c2476f3c898879fa6be89c18e1ea325bf88c13 Mon Sep 17 00:00:00 2001 From: Tobias Skarhed Date: Tue, 31 Jul 2018 09:35:08 +0200 Subject: [PATCH 226/786] Weird execution order for the tests... --- .../datasource/prometheus/datasource.ts | 7 +++++- .../prometheus/result_transformer.ts | 7 +++++- .../prometheus/specs/_datasource.jest.ts | 25 +++---------------- 3 files changed, 15 insertions(+), 24 deletions(-) diff --git a/public/app/plugins/datasource/prometheus/datasource.ts b/public/app/plugins/datasource/prometheus/datasource.ts index 75a946d6f36..6801a9a1d59 100644 --- a/public/app/plugins/datasource/prometheus/datasource.ts +++ b/public/app/plugins/datasource/prometheus/datasource.ts @@ -175,8 +175,12 @@ export class PrometheusDatasource { responseIndex: index, refId: activeTargets[index].refId, }; - + console.log('format: ' + transformerOptions.format); + console.log('resultType: ' + response.data.data.resultType); + console.log('legendFormat: ' + transformerOptions.legendFormat); + // console.log(result); this.resultTransformer.transform(result, response, transformerOptions); + // console.log(result); }); return { data: result }; @@ -233,6 +237,7 @@ export class PrometheusDatasource { if (start > end) { throw { message: 'Invalid time range' }; } + // console.log(query.expr); var url = '/api/v1/query_range'; var data = { diff --git a/public/app/plugins/datasource/prometheus/result_transformer.ts b/public/app/plugins/datasource/prometheus/result_transformer.ts index b6d8a32af5f..4b69cb98c54 100644 --- a/public/app/plugins/datasource/prometheus/result_transformer.ts +++ b/public/app/plugins/datasource/prometheus/result_transformer.ts @@ -6,7 +6,9 @@ export class ResultTransformer { transform(result: any, response: any, options: any) { let prometheusResult = response.data.data.result; - + console.log(prometheusResult); + // console.log(options); + // console.log(result); if (options.format === 'table') { result.push(this.transformMetricDataToTable(prometheusResult, options.responseListLength, options.refId)); } else if (options.format === 'heatmap') { @@ -26,6 +28,7 @@ export class ResultTransformer { } } } + // console.log(result); } transformMetricData(metricData, options, start, end) { @@ -137,6 +140,7 @@ export class ResultTransformer { if (!label || label === '{}') { label = options.query; } + console.log(label); return label; } @@ -156,6 +160,7 @@ export class ResultTransformer { var labelPart = _.map(_.toPairs(labelData), function(label) { return label[0] + '="' + label[1] + '"'; }).join(','); + console.log(metricName); return metricName + '{' + labelPart + '}'; } diff --git a/public/app/plugins/datasource/prometheus/specs/_datasource.jest.ts b/public/app/plugins/datasource/prometheus/specs/_datasource.jest.ts index 384abc8f902..34f78585d76 100644 --- a/public/app/plugins/datasource/prometheus/specs/_datasource.jest.ts +++ b/public/app/plugins/datasource/prometheus/specs/_datasource.jest.ts @@ -21,23 +21,7 @@ let backendSrv = { }; let templateSrv = { - replace: (target, scopedVars, format) => { - if (!target) { - return target; - } - let variable, value, fmt; - - return target.replace(scopedVars, (match, var1, var2, fmt2, var3, fmt3) => { - variable = this.index[var1 || var2 || var3]; - fmt = fmt2 || fmt3 || format; - if (scopedVars) { - value = scopedVars[var1 || var2 || var3]; - if (value) { - return this.formatValue(value.value, fmt, variable); - } - } - }); - }, + replace: jest.fn(str => str), }; let timeSrv = { @@ -63,10 +47,7 @@ describe('PrometheusDatasource', function() { // }) // ); - beforeEach(() => { - ctx.ds = new PrometheusDatasource(instanceSettings, $q, backendSrv, templateSrv, timeSrv); - }); - describe('When querying prometheus with one target using query editor target spec', function() { + describe('When querying prometheus with one target using query editor target spec', async () => { var results; var query = { range: { from: time({ seconds: 63 }), to: time({ seconds: 183 }) }, @@ -106,7 +87,7 @@ describe('PrometheusDatasource', function() { expect(res.method).toBe('GET'); expect(res.url).toBe(urlExpected); }); - it('should return series list', function() { + it('should return series list', async () => { expect(results.data.length).toBe(1); expect(results.data[0].target).toBe('test{job="testjob"}'); }); From f1f0400769f01c99101914cb1ba62cca0e64ac94 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Tue, 31 Jul 2018 11:41:58 +0200 Subject: [PATCH 227/786] changelog: add notes about closing #12300 [skip ci] --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 11baca97714..d3532ebe640 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,7 @@ * **Alerting**: Fix diff and percent_diff reducers [#11563](https://github.com/grafana/grafana/issues/11563), thx [@jessetane](https://github.com/jessetane) * **Units**: Polish złoty currency [#12691](https://github.com/grafana/grafana/pull/12691), thx [@mwegrzynek](https://github.com/mwegrzynek) * **Cloudwatch**: Improved error handling [#12489](https://github.com/grafana/grafana/issues/12489), thx [@mtanda](https://github.com/mtanda) +* **Cloudwatch**: AWS/AppSync metrics and dimensions [#12300](https://github.com/grafana/grafana/issues/12300), thx [@franciscocpg](https://github.com/franciscocpg) * **Table**: Adjust header contrast for the light theme [#12668](https://github.com/grafana/grafana/issues/12668) * **Elasticsearch**: For alerting/backend, support having index name to the right of pattern in index pattern [#12731](https://github.com/grafana/grafana/issues/12731) * **OAuth**: Fix overriding tls_skip_verify_insecure using environment variable [#12747](https://github.com/grafana/grafana/issues/12747), thx [@jangaraj](https://github.com/jangaraj) From 7b5b94607b2956ab81d05c34fbb4c2e2fc615ab7 Mon Sep 17 00:00:00 2001 From: Patrick O'Carroll Date: Tue, 31 Jul 2018 12:51:07 +0200 Subject: [PATCH 228/786] fixed color for links in colored cells by adding a new variable that sets color: white when cell or row has background-color --- public/app/plugins/panel/table/renderer.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/public/app/plugins/panel/table/renderer.ts b/public/app/plugins/panel/table/renderer.ts index f6950dada52..456dadf6241 100644 --- a/public/app/plugins/panel/table/renderer.ts +++ b/public/app/plugins/panel/table/renderer.ts @@ -214,15 +214,20 @@ export class TableRenderer { var style = ''; var cellClasses = []; var cellClass = ''; + var linkStyle = ''; + + if (this.colorState.row) { + linkStyle = ' style="color: white"'; + } if (this.colorState.cell) { style = ' style="background-color:' + this.colorState.cell + ';color: white"'; + linkStyle = ' style="color: white;"'; this.colorState.cell = null; } else if (this.colorState.value) { style = ' style="color:' + this.colorState.value + '"'; this.colorState.value = null; } - // 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) @@ -253,7 +258,7 @@ export class TableRenderer { cellClasses.push('table-panel-cell-link'); columnHtml += ` - + ${value} `; From 4b8ec4e32330b9fd606acc39604a8b9de7229ac3 Mon Sep 17 00:00:00 2001 From: Patrick O'Carroll Date: Tue, 31 Jul 2018 13:07:43 +0200 Subject: [PATCH 229/786] removed a blank space in div --- public/app/plugins/panel/table/renderer.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/app/plugins/panel/table/renderer.ts b/public/app/plugins/panel/table/renderer.ts index 456dadf6241..c1e4e6243f9 100644 --- a/public/app/plugins/panel/table/renderer.ts +++ b/public/app/plugins/panel/table/renderer.ts @@ -258,7 +258,7 @@ export class TableRenderer { cellClasses.push('table-panel-cell-link'); columnHtml += ` - + ${value} `; From 276a5e6eb5603df07d48aa66af4763bc9f3576c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Tue, 31 Jul 2018 17:29:02 +0200 Subject: [PATCH 230/786] fix: test data api route used old name for test data datasource, fixes #12773 --- pkg/api/metrics.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/api/metrics.go b/pkg/api/metrics.go index 00ad25ab8c2..f2bc79df7ad 100644 --- a/pkg/api/metrics.go +++ b/pkg/api/metrics.go @@ -99,7 +99,7 @@ func GetTestDataRandomWalk(c *m.ReqContext) Response { timeRange := tsdb.NewTimeRange(from, to) request := &tsdb.TsdbQuery{TimeRange: timeRange} - dsInfo := &m.DataSource{Type: "grafana-testdata-datasource"} + dsInfo := &m.DataSource{Type: "testdata"} request.Queries = append(request.Queries, &tsdb.Query{ RefId: "A", IntervalMs: intervalMs, From 89eae1566d036e153aea18eb62e983bc21bd315f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Tue, 31 Jul 2018 17:31:45 +0200 Subject: [PATCH 231/786] fix: team email tooltip was not showing --- public/app/core/components/Forms/Forms.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/app/core/components/Forms/Forms.tsx b/public/app/core/components/Forms/Forms.tsx index 4b74d48ba08..543e1a1d6df 100644 --- a/public/app/core/components/Forms/Forms.tsx +++ b/public/app/core/components/Forms/Forms.tsx @@ -12,7 +12,7 @@ export const Label: SFC = props => { {props.children} {props.tooltip && ( - + )} From 6df3722a35faf455e2d25989a80a8e167531b5b7 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Tue, 31 Jul 2018 18:01:36 +0200 Subject: [PATCH 232/786] changelog: add notes about closing #12762 [skip ci] --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d3532ebe640..dde7ead6f13 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,7 +23,8 @@ * **Alerting**: Fix diff and percent_diff reducers [#11563](https://github.com/grafana/grafana/issues/11563), thx [@jessetane](https://github.com/jessetane) * **Units**: Polish złoty currency [#12691](https://github.com/grafana/grafana/pull/12691), thx [@mwegrzynek](https://github.com/mwegrzynek) * **Cloudwatch**: Improved error handling [#12489](https://github.com/grafana/grafana/issues/12489), thx [@mtanda](https://github.com/mtanda) -* **Cloudwatch**: AWS/AppSync metrics and dimensions [#12300](https://github.com/grafana/grafana/issues/12300), thx [@franciscocpg](https://github.com/franciscocpg) +* **Cloudwatch**: AppSync metrics and dimensions [#12300](https://github.com/grafana/grafana/issues/12300), thx [@franciscocpg](https://github.com/franciscocpg) +* **Cloudwatch**: Direct Connect metrics and dimensions [#12762](https://github.com/grafana/grafana/pulls/12762), thx [@mindriot88](https://github.com/mindriot88) * **Table**: Adjust header contrast for the light theme [#12668](https://github.com/grafana/grafana/issues/12668) * **Elasticsearch**: For alerting/backend, support having index name to the right of pattern in index pattern [#12731](https://github.com/grafana/grafana/issues/12731) * **OAuth**: Fix overriding tls_skip_verify_insecure using environment variable [#12747](https://github.com/grafana/grafana/issues/12747), thx [@jangaraj](https://github.com/jangaraj) From 43295f9c189e5fd5539892162562be7d33046603 Mon Sep 17 00:00:00 2001 From: Sven Klemm Date: Mon, 30 Jul 2018 13:23:29 +0200 Subject: [PATCH 233/786] remove alias from postgres $__timeGroup macro --- docs/sources/features/datasources/postgres.md | 2 +- pkg/tsdb/postgres/macros.go | 2 +- pkg/tsdb/postgres/macros_test.go | 4 ++-- pkg/tsdb/postgres/postgres_test.go | 6 +++--- .../plugins/datasource/postgres/partials/query.editor.html | 2 +- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/docs/sources/features/datasources/postgres.md b/docs/sources/features/datasources/postgres.md index 793b3b6f4c0..7915f29fcdc 100644 --- a/docs/sources/features/datasources/postgres.md +++ b/docs/sources/features/datasources/postgres.md @@ -60,7 +60,7 @@ Macro example | Description *$__timeFilter(dateColumn)* | Will be replaced by a time range filter using the specified column name. For example, *dateColumn BETWEEN '2017-04-21T05:01:17Z' AND '2017-04-21T05:06:17Z'* *$__timeFrom()* | Will be replaced by the start of the currently active time selection. For example, *'2017-04-21T05:01:17Z'* *$__timeTo()* | Will be replaced by the end of the currently active time selection. For example, *'2017-04-21T05:06:17Z'* -*$__timeGroup(dateColumn,'5m')* | Will be replaced by an expression usable in GROUP BY clause. For example, *(extract(epoch from dateColumn)/300)::bigint*300 AS time* +*$__timeGroup(dateColumn,'5m')* | Will be replaced by an expression usable in GROUP BY clause. For example, *(extract(epoch from dateColumn)/300)::bigint*300* *$__timeGroup(dateColumn,'5m', 0)* | Same as above but with a fill parameter so all null values will be converted to the fill value (all null values would be set to zero using this example). *$__unixEpochFilter(dateColumn)* | Will be replaced by a time range filter using the specified column name with times represented as unix timestamp. For example, *dateColumn >= 1494410783 AND dateColumn <= 1494497183* *$__unixEpochFrom()* | Will be replaced by the start of the currently active time selection as unix timestamp. For example, *1494410783* diff --git a/pkg/tsdb/postgres/macros.go b/pkg/tsdb/postgres/macros.go index 661dbf3d4ce..852e9d7997e 100644 --- a/pkg/tsdb/postgres/macros.go +++ b/pkg/tsdb/postgres/macros.go @@ -109,7 +109,7 @@ func (m *postgresMacroEngine) evaluateMacro(name string, args []string) (string, m.query.Model.Set("fillValue", floatVal) } } - return fmt.Sprintf("floor(extract(epoch from %s)/%v)*%v AS time", args[0], interval.Seconds(), interval.Seconds()), nil + return fmt.Sprintf("floor(extract(epoch from %s)/%v)*%v", args[0], interval.Seconds(), interval.Seconds()), nil case "__unixEpochFilter": if len(args) == 0 { return "", fmt.Errorf("missing time column argument for macro %v", name) diff --git a/pkg/tsdb/postgres/macros_test.go b/pkg/tsdb/postgres/macros_test.go index 194573be0fd..bb947d4f01f 100644 --- a/pkg/tsdb/postgres/macros_test.go +++ b/pkg/tsdb/postgres/macros_test.go @@ -53,7 +53,7 @@ func TestMacroEngine(t *testing.T) { sql, err := engine.Interpolate(query, timeRange, "GROUP BY $__timeGroup(time_column,'5m')") So(err, ShouldBeNil) - So(sql, ShouldEqual, "GROUP BY floor(extract(epoch from time_column)/300)*300 AS time") + So(sql, ShouldEqual, "GROUP BY floor(extract(epoch from time_column)/300)*300") }) Convey("interpolate __timeGroup function with spaces between args", func() { @@ -61,7 +61,7 @@ func TestMacroEngine(t *testing.T) { sql, err := engine.Interpolate(query, timeRange, "GROUP BY $__timeGroup(time_column , '5m')") So(err, ShouldBeNil) - So(sql, ShouldEqual, "GROUP BY floor(extract(epoch from time_column)/300)*300 AS time") + So(sql, ShouldEqual, "GROUP BY floor(extract(epoch from time_column)/300)*300") }) Convey("interpolate __timeTo function", func() { diff --git a/pkg/tsdb/postgres/postgres_test.go b/pkg/tsdb/postgres/postgres_test.go index c7787929a9d..3e864dca1e6 100644 --- a/pkg/tsdb/postgres/postgres_test.go +++ b/pkg/tsdb/postgres/postgres_test.go @@ -183,7 +183,7 @@ func TestPostgres(t *testing.T) { Queries: []*tsdb.Query{ { Model: simplejson.NewFromAny(map[string]interface{}{ - "rawSql": "SELECT $__timeGroup(time, '5m'), avg(value) as value FROM metric GROUP BY 1 ORDER BY 1", + "rawSql": "SELECT $__timeGroup(time, '5m') AS time, avg(value) as value FROM metric GROUP BY 1 ORDER BY 1", "format": "time_series", }), RefId: "A", @@ -227,7 +227,7 @@ func TestPostgres(t *testing.T) { Queries: []*tsdb.Query{ { Model: simplejson.NewFromAny(map[string]interface{}{ - "rawSql": "SELECT $__timeGroup(time, '5m', NULL), avg(value) as value FROM metric GROUP BY 1 ORDER BY 1", + "rawSql": "SELECT $__timeGroup(time, '5m', NULL) AS time, avg(value) as value FROM metric GROUP BY 1 ORDER BY 1", "format": "time_series", }), RefId: "A", @@ -281,7 +281,7 @@ func TestPostgres(t *testing.T) { Queries: []*tsdb.Query{ { Model: simplejson.NewFromAny(map[string]interface{}{ - "rawSql": "SELECT $__timeGroup(time, '5m', 1.5), avg(value) as value FROM metric GROUP BY 1 ORDER BY 1", + "rawSql": "SELECT $__timeGroup(time, '5m', 1.5) AS time, avg(value) as value FROM metric GROUP BY 1 ORDER BY 1", "format": "time_series", }), RefId: "A", diff --git a/public/app/plugins/datasource/postgres/partials/query.editor.html b/public/app/plugins/datasource/postgres/partials/query.editor.html index b7c12471f52..1ace05abae2 100644 --- a/public/app/plugins/datasource/postgres/partials/query.editor.html +++ b/public/app/plugins/datasource/postgres/partials/query.editor.html @@ -53,7 +53,7 @@ Macros: - $__timeEpoch -> extract(epoch from column) as "time" - $__timeFilter(column) -> column BETWEEN '2017-04-21T05:01:17Z' AND '2017-04-21T05:01:17Z' - $__unixEpochFilter(column) -> column >= 1492750877 AND column <= 1492750877 -- $__timeGroup(column,'5m') -> (extract(epoch from column)/300)::bigint*300 AS time +- $__timeGroup(column,'5m') -> (extract(epoch from column)/300)::bigint*300 Example of group by and order by with $__timeGroup: SELECT From bd77541e092e022166a265d81e26583f6305de14 Mon Sep 17 00:00:00 2001 From: Sven Klemm Date: Wed, 1 Aug 2018 08:00:43 +0200 Subject: [PATCH 234/786] adjust test dashboards --- .../datasource_tests_postgres_unittest.json | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/devenv/dev-dashboards/datasource_tests_postgres_unittest.json b/devenv/dev-dashboards/datasource_tests_postgres_unittest.json index 2243baed0aa..a3139bf99f7 100644 --- a/devenv/dev-dashboards/datasource_tests_postgres_unittest.json +++ b/devenv/dev-dashboards/datasource_tests_postgres_unittest.json @@ -369,7 +369,7 @@ { "alias": "", "format": "time_series", - "rawSql": "SELECT $__timeGroup(time, '5m'), avg(value) as value FROM metric WHERE $__timeFilter(time) GROUP BY 1 ORDER BY 1", + "rawSql": "SELECT $__timeGroup(time, '5m') AS time, avg(value) as value FROM metric WHERE $__timeFilter(time) GROUP BY 1 ORDER BY 1", "refId": "A" } ], @@ -452,7 +452,7 @@ { "alias": "", "format": "time_series", - "rawSql": "SELECT $__timeGroup(time, '5m', NULL), avg(value) as value FROM metric WHERE $__timeFilter(time) GROUP BY 1 ORDER BY 1", + "rawSql": "SELECT $__timeGroup(time, '5m', NULL) AS time, avg(value) as value FROM metric WHERE $__timeFilter(time) GROUP BY 1 ORDER BY 1", "refId": "A" } ], @@ -535,7 +535,7 @@ { "alias": "", "format": "time_series", - "rawSql": "SELECT $__timeGroup(time, '5m', 10.0), avg(value) as value FROM metric WHERE $__timeFilter(time) GROUP BY 1 ORDER BY 1", + "rawSql": "SELECT $__timeGroup(time, '5m', 10.0) AS time, avg(value) as value FROM metric WHERE $__timeFilter(time) GROUP BY 1 ORDER BY 1", "refId": "A" } ], @@ -618,7 +618,7 @@ { "alias": "", "format": "time_series", - "rawSql": "SELECT $__timeGroup(time, '$summarize'), avg(value) as value FROM metric WHERE $__timeFilter(time) GROUP BY 1 ORDER BY 1", + "rawSql": "SELECT $__timeGroup(time, '$summarize') AS time, avg(value) as value FROM metric WHERE $__timeFilter(time) GROUP BY 1 ORDER BY 1", "refId": "A" } ], @@ -701,7 +701,7 @@ { "alias": "", "format": "time_series", - "rawSql": "SELECT $__timeGroup(time, '$summarize', NULL), sum(value) as value FROM metric WHERE $__timeFilter(time) GROUP BY 1 ORDER BY 1", + "rawSql": "SELECT $__timeGroup(time, '$summarize', NULL) AS time, sum(value) as value FROM metric WHERE $__timeFilter(time) GROUP BY 1 ORDER BY 1", "refId": "A" } ], @@ -784,7 +784,7 @@ { "alias": "", "format": "time_series", - "rawSql": "SELECT $__timeGroup(time, '$summarize', 100.0), sum(value) as value FROM metric WHERE $__timeFilter(time) GROUP BY 1 ORDER BY 1", + "rawSql": "SELECT $__timeGroup(time, '$summarize', 100.0) AS time, sum(value) as value FROM metric WHERE $__timeFilter(time) GROUP BY 1 ORDER BY 1", "refId": "A" } ], @@ -871,7 +871,7 @@ { "alias": "", "format": "time_series", - "rawSql": "SELECT \n $__timeGroup(time, '$summarize'), \n measurement, \n avg(\"valueOne\") as \"valueOne\",\n avg(\"valueTwo\") as \"valueTwo\"\nFROM\n metric_values \nWHERE\n $__timeFilter(time) AND\n measurement in($metric)\nGROUP BY 1, 2\nORDER BY 1", + "rawSql": "SELECT \n $__timeGroupAlias(time, '$summarize'), \n measurement, \n avg(\"valueOne\") as \"valueOne\",\n avg(\"valueTwo\") as \"valueTwo\"\nFROM\n metric_values \nWHERE\n $__timeFilter(time) AND\n measurement in($metric)\nGROUP BY 1, 2\nORDER BY 1", "refId": "A" } ], @@ -956,7 +956,7 @@ { "alias": "", "format": "time_series", - "rawSql": "SELECT \n $__timeGroup(time, '$summarize'), \n avg(\"valueOne\") as \"valueOne\", \n avg(\"valueTwo\") as \"valueTwo\" \nFROM\n metric_values \nWHERE\n $__timeFilter(time) AND\n measurement in($metric)\nGROUP BY 1\nORDER BY 1", + "rawSql": "SELECT \n $__timeGroup(time, '$summarize') AS time, \n avg(\"valueOne\") as \"valueOne\", \n avg(\"valueTwo\") as \"valueTwo\" \nFROM\n metric_values \nWHERE\n $__timeFilter(time) AND\n measurement in($metric)\nGROUP BY 1\nORDER BY 1", "refId": "A" } ], @@ -2352,5 +2352,5 @@ "timezone": "", "title": "Datasource tests - Postgres (unittest)", "uid": "vHQdlVziz", - "version": 1 -} \ No newline at end of file + "version": 17 +} From 42f189282618fb5ce42efc7f8cf804bdb1da65da Mon Sep 17 00:00:00 2001 From: Sven Klemm Date: Wed, 1 Aug 2018 08:48:22 +0200 Subject: [PATCH 235/786] Add $__timeGroupAlias to postgres macros --- .../datasource_tests_postgres_unittest.json | 17 +++++++++-------- pkg/tsdb/postgres/macros.go | 6 ++++++ pkg/tsdb/postgres/macros_test.go | 14 ++++++++++---- 3 files changed, 25 insertions(+), 12 deletions(-) diff --git a/devenv/dev-dashboards/datasource_tests_postgres_unittest.json b/devenv/dev-dashboards/datasource_tests_postgres_unittest.json index a3139bf99f7..3c2b34df78c 100644 --- a/devenv/dev-dashboards/datasource_tests_postgres_unittest.json +++ b/devenv/dev-dashboards/datasource_tests_postgres_unittest.json @@ -369,7 +369,7 @@ { "alias": "", "format": "time_series", - "rawSql": "SELECT $__timeGroup(time, '5m') AS time, avg(value) as value FROM metric WHERE $__timeFilter(time) GROUP BY 1 ORDER BY 1", + "rawSql": "SELECT $__timeGroupAlias(time, '5m'), avg(value) as value FROM metric WHERE $__timeFilter(time) GROUP BY 1 ORDER BY 1", "refId": "A" } ], @@ -452,7 +452,7 @@ { "alias": "", "format": "time_series", - "rawSql": "SELECT $__timeGroup(time, '5m', NULL) AS time, avg(value) as value FROM metric WHERE $__timeFilter(time) GROUP BY 1 ORDER BY 1", + "rawSql": "SELECT $__timeGroupAlias(time, '5m', NULL), avg(value) as value FROM metric WHERE $__timeFilter(time) GROUP BY 1 ORDER BY 1", "refId": "A" } ], @@ -535,7 +535,7 @@ { "alias": "", "format": "time_series", - "rawSql": "SELECT $__timeGroup(time, '5m', 10.0) AS time, avg(value) as value FROM metric WHERE $__timeFilter(time) GROUP BY 1 ORDER BY 1", + "rawSql": "SELECT $__timeGroupAlias(time, '5m', 10.0), avg(value) as value FROM metric WHERE $__timeFilter(time) GROUP BY 1 ORDER BY 1", "refId": "A" } ], @@ -618,7 +618,7 @@ { "alias": "", "format": "time_series", - "rawSql": "SELECT $__timeGroup(time, '$summarize') AS time, avg(value) as value FROM metric WHERE $__timeFilter(time) GROUP BY 1 ORDER BY 1", + "rawSql": "SELECT $__timeGroupAlias(time, '$summarize'), avg(value) as value FROM metric WHERE $__timeFilter(time) GROUP BY 1 ORDER BY 1", "refId": "A" } ], @@ -701,7 +701,7 @@ { "alias": "", "format": "time_series", - "rawSql": "SELECT $__timeGroup(time, '$summarize', NULL) AS time, sum(value) as value FROM metric WHERE $__timeFilter(time) GROUP BY 1 ORDER BY 1", + "rawSql": "SELECT $__timeGroupAlias(time, '$summarize', NULL), sum(value) as value FROM metric WHERE $__timeFilter(time) GROUP BY 1 ORDER BY 1", "refId": "A" } ], @@ -784,7 +784,7 @@ { "alias": "", "format": "time_series", - "rawSql": "SELECT $__timeGroup(time, '$summarize', 100.0) AS time, sum(value) as value FROM metric WHERE $__timeFilter(time) GROUP BY 1 ORDER BY 1", + "rawSql": "SELECT $__timeGroupAlias(time, '$summarize', 100.0), sum(value) as value FROM metric WHERE $__timeFilter(time) GROUP BY 1 ORDER BY 1", "refId": "A" } ], @@ -956,7 +956,7 @@ { "alias": "", "format": "time_series", - "rawSql": "SELECT \n $__timeGroup(time, '$summarize') AS time, \n avg(\"valueOne\") as \"valueOne\", \n avg(\"valueTwo\") as \"valueTwo\" \nFROM\n metric_values \nWHERE\n $__timeFilter(time) AND\n measurement in($metric)\nGROUP BY 1\nORDER BY 1", + "rawSql": "SELECT \n $__timeGroupAlias(time, '$summarize'), \n avg(\"valueOne\") as \"valueOne\", \n avg(\"valueTwo\") as \"valueTwo\" \nFROM\n metric_values \nWHERE\n $__timeFilter(time) AND\n measurement in($metric)\nGROUP BY 1\nORDER BY 1", "refId": "A" } ], @@ -2352,5 +2352,6 @@ "timezone": "", "title": "Datasource tests - Postgres (unittest)", "uid": "vHQdlVziz", - "version": 17 + "version": 1 } + diff --git a/pkg/tsdb/postgres/macros.go b/pkg/tsdb/postgres/macros.go index 852e9d7997e..fa887032c5d 100644 --- a/pkg/tsdb/postgres/macros.go +++ b/pkg/tsdb/postgres/macros.go @@ -110,6 +110,12 @@ func (m *postgresMacroEngine) evaluateMacro(name string, args []string) (string, } } return fmt.Sprintf("floor(extract(epoch from %s)/%v)*%v", args[0], interval.Seconds(), interval.Seconds()), nil + case "__timeGroupAlias": + tg, err := m.evaluateMacro("__timeGroup", args) + if err == nil { + return tg + " AS \"time\"", err + } + return "", err case "__unixEpochFilter": if len(args) == 0 { return "", fmt.Errorf("missing time column argument for macro %v", name) diff --git a/pkg/tsdb/postgres/macros_test.go b/pkg/tsdb/postgres/macros_test.go index bb947d4f01f..ec74470a803 100644 --- a/pkg/tsdb/postgres/macros_test.go +++ b/pkg/tsdb/postgres/macros_test.go @@ -50,18 +50,24 @@ func TestMacroEngine(t *testing.T) { Convey("interpolate __timeGroup function", func() { - sql, err := engine.Interpolate(query, timeRange, "GROUP BY $__timeGroup(time_column,'5m')") + sql, err := engine.Interpolate(query, timeRange, "$__timeGroup(time_column,'5m')") + So(err, ShouldBeNil) + sql2, err := engine.Interpolate(query, timeRange, "$__timeGroupAlias(time_column,'5m')") So(err, ShouldBeNil) - So(sql, ShouldEqual, "GROUP BY floor(extract(epoch from time_column)/300)*300") + So(sql, ShouldEqual, "floor(extract(epoch from time_column)/300)*300") + So(sql2, ShouldEqual, sql+" AS \"time\"") }) Convey("interpolate __timeGroup function with spaces between args", func() { - sql, err := engine.Interpolate(query, timeRange, "GROUP BY $__timeGroup(time_column , '5m')") + sql, err := engine.Interpolate(query, timeRange, "$__timeGroup(time_column , '5m')") + So(err, ShouldBeNil) + sql2, err := engine.Interpolate(query, timeRange, "$__timeGroupAlias(time_column , '5m')") So(err, ShouldBeNil) - So(sql, ShouldEqual, "GROUP BY floor(extract(epoch from time_column)/300)*300") + So(sql, ShouldEqual, "floor(extract(epoch from time_column)/300)*300") + So(sql2, ShouldEqual, sql+" AS \"time\"") }) Convey("interpolate __timeTo function", func() { From d4d896ade829300fa306bac82798d746a85e9693 Mon Sep 17 00:00:00 2001 From: Patrick O'Carroll Date: Wed, 1 Aug 2018 09:08:17 +0200 Subject: [PATCH 236/786] replaced style with class for links --- public/app/plugins/panel/table/renderer.ts | 13 +++++++++---- .../app/plugins/panel/table/specs/renderer.jest.ts | 2 +- public/sass/components/_panel_table.scss | 4 ++++ 3 files changed, 14 insertions(+), 5 deletions(-) diff --git a/public/app/plugins/panel/table/renderer.ts b/public/app/plugins/panel/table/renderer.ts index c1e4e6243f9..474e9c89493 100644 --- a/public/app/plugins/panel/table/renderer.ts +++ b/public/app/plugins/panel/table/renderer.ts @@ -214,15 +214,15 @@ export class TableRenderer { var style = ''; var cellClasses = []; var cellClass = ''; - var linkStyle = ''; + var linkClass = ''; if (this.colorState.row) { - linkStyle = ' style="color: white"'; + linkClass = 'table-panel-link'; } if (this.colorState.cell) { style = ' style="background-color:' + this.colorState.cell + ';color: white"'; - linkStyle = ' style="color: white;"'; + linkClass = 'table-panel-link'; this.colorState.cell = null; } else if (this.colorState.value) { style = ' style="color:' + this.colorState.value + '"'; @@ -258,7 +258,12 @@ export class TableRenderer { cellClasses.push('table-panel-cell-link'); columnHtml += ` - + ${value} `; diff --git a/public/app/plugins/panel/table/specs/renderer.jest.ts b/public/app/plugins/panel/table/specs/renderer.jest.ts index 22957d1aa66..f1a686fb739 100644 --- a/public/app/plugins/panel/table/specs/renderer.jest.ts +++ b/public/app/plugins/panel/table/specs/renderer.jest.ts @@ -268,7 +268,7 @@ describe('when rendering table', () => { var expectedHtml = `
    diff --git a/public/sass/components/_panel_table.scss b/public/sass/components/_panel_table.scss index 8e0ecf15896..99e91f8ff67 100644 --- a/public/sass/components/_panel_table.scss +++ b/public/sass/components/_panel_table.scss @@ -133,3 +133,7 @@ height: 0px; line-height: 0px; } + +.table-panel-link { + color: white; +} From d6158bc2935ec396f45114d736e684bb3a522c6b Mon Sep 17 00:00:00 2001 From: Tobias Skarhed Date: Wed, 1 Aug 2018 09:30:26 +0200 Subject: [PATCH 237/786] All tests passing --- .../datasource/prometheus/datasource.ts | 6 - .../prometheus/result_transformer.ts | 7 +- .../prometheus/specs/_datasource.jest.ts | 333 +++++---- .../prometheus/specs/datasource_specs.ts | 683 ------------------ 4 files changed, 196 insertions(+), 833 deletions(-) delete mode 100644 public/app/plugins/datasource/prometheus/specs/datasource_specs.ts diff --git a/public/app/plugins/datasource/prometheus/datasource.ts b/public/app/plugins/datasource/prometheus/datasource.ts index 6801a9a1d59..ac8d774db59 100644 --- a/public/app/plugins/datasource/prometheus/datasource.ts +++ b/public/app/plugins/datasource/prometheus/datasource.ts @@ -175,12 +175,7 @@ export class PrometheusDatasource { responseIndex: index, refId: activeTargets[index].refId, }; - console.log('format: ' + transformerOptions.format); - console.log('resultType: ' + response.data.data.resultType); - console.log('legendFormat: ' + transformerOptions.legendFormat); - // console.log(result); this.resultTransformer.transform(result, response, transformerOptions); - // console.log(result); }); return { data: result }; @@ -237,7 +232,6 @@ export class PrometheusDatasource { if (start > end) { throw { message: 'Invalid time range' }; } - // console.log(query.expr); var url = '/api/v1/query_range'; var data = { diff --git a/public/app/plugins/datasource/prometheus/result_transformer.ts b/public/app/plugins/datasource/prometheus/result_transformer.ts index 4b69cb98c54..b6d8a32af5f 100644 --- a/public/app/plugins/datasource/prometheus/result_transformer.ts +++ b/public/app/plugins/datasource/prometheus/result_transformer.ts @@ -6,9 +6,7 @@ export class ResultTransformer { transform(result: any, response: any, options: any) { let prometheusResult = response.data.data.result; - console.log(prometheusResult); - // console.log(options); - // console.log(result); + if (options.format === 'table') { result.push(this.transformMetricDataToTable(prometheusResult, options.responseListLength, options.refId)); } else if (options.format === 'heatmap') { @@ -28,7 +26,6 @@ export class ResultTransformer { } } } - // console.log(result); } transformMetricData(metricData, options, start, end) { @@ -140,7 +137,6 @@ export class ResultTransformer { if (!label || label === '{}') { label = options.query; } - console.log(label); return label; } @@ -160,7 +156,6 @@ export class ResultTransformer { var labelPart = _.map(_.toPairs(labelData), function(label) { return label[0] + '="' + label[1] + '"'; }).join(','); - console.log(metricName); return metricName + '{' + labelPart + '}'; } diff --git a/public/app/plugins/datasource/prometheus/specs/_datasource.jest.ts b/public/app/plugins/datasource/prometheus/specs/_datasource.jest.ts index 34f78585d76..2deab13a101 100644 --- a/public/app/plugins/datasource/prometheus/specs/_datasource.jest.ts +++ b/public/app/plugins/datasource/prometheus/specs/_datasource.jest.ts @@ -1,6 +1,7 @@ import moment from 'moment'; import { PrometheusDatasource } from '../datasource'; import $q from 'q'; +import { angularMocks } from 'test/lib/common'; const SECOND = 1000; const MINUTE = 60 * SECOND; @@ -57,32 +58,31 @@ describe('PrometheusDatasource', function() { // Interval alignment with step var urlExpected = 'proxied/api/v1/query_range?query=' + encodeURIComponent('test{job="testjob"}') + '&start=60&end=240&step=60'; - var response = { - data: { - status: 'success', - data: { - resultType: 'matrix', - result: [ - { - metric: { __name__: 'test', job: 'testjob' }, - values: [[60, '3846']], - }, - ], - }, - }, - }; + beforeEach(async () => { - // ctx.$httpBackend.expect('GET', urlExpected).respond(response); + let response = { + data: { + status: 'success', + data: { + resultType: 'matrix', + result: [ + { + metric: { __name__: 'test', job: 'testjob' }, + values: [[60, '3846']], + }, + ], + }, + }, + }; backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response)); ctx.ds = new PrometheusDatasource(instanceSettings, $q, backendSrv, templateSrv, timeSrv); await ctx.ds.query(query).then(function(data) { results = data; }); - // ctx.$httpBackend.flush(); }); + it('should generate the correct query', function() { - // ctx.$httpBackend.verifyNoOutstandingExpectation(); let res = backendSrv.datasourceRequest.mock.calls[0][0]; expect(res.method).toBe('GET'); expect(res.url).toBe(urlExpected); @@ -97,39 +97,33 @@ describe('PrometheusDatasource', function() { var start = 60; var end = 360; var step = 60; - // var urlExpected = - // 'proxied/api/v1/query_range?query=' + - // encodeURIComponent('test{job="testjob"}') + - // '&start=' + - // start + - // '&end=' + - // end + - // '&step=' + - // step; + var query = { range: { from: time({ seconds: start }), to: time({ seconds: end }) }, targets: [{ expr: 'test{job="testjob"}', format: 'time_series' }], interval: '60s', }; - var response = { - status: 'success', - data: { - data: { - resultType: 'matrix', - result: [ - { - metric: { __name__: 'test', job: 'testjob', series: 'series 1' }, - values: [[start + step * 1, '3846'], [start + step * 3, '3847'], [end - step * 1, '3848']], - }, - { - metric: { __name__: 'test', job: 'testjob', series: 'series 2' }, - values: [[start + step * 2, '4846']], - }, - ], - }, - }, - }; + beforeEach(async () => { + let response = { + status: 'success', + data: { + data: { + resultType: 'matrix', + result: [ + { + metric: { __name__: 'test', job: 'testjob', series: 'series 1' }, + values: [[start + step * 1, '3846'], [start + step * 3, '3847'], [end - step * 1, '3848']], + }, + { + metric: { __name__: 'test', job: 'testjob', series: 'series 2' }, + values: [[start + step * 2, '4846']], + }, + ], + }, + }, + }; + backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response)); ctx.ds = new PrometheusDatasource(instanceSettings, $q, backendSrv, templateSrv, timeSrv); @@ -137,11 +131,13 @@ describe('PrometheusDatasource', function() { results = data; }); }); + it('should be same length', function() { expect(results.data.length).toBe(2); expect(results.data[0].datapoints.length).toBe((end - start) / step + 1); expect(results.data[1].datapoints.length).toBe((end - start) / step + 1); }); + it('should fill null until first datapoint in response', function() { expect(results.data[0].datapoints[0][1]).toBe(start * 1000); expect(results.data[0].datapoints[0][0]).toBe(null); @@ -172,21 +168,23 @@ describe('PrometheusDatasource', function() { targets: [{ expr: 'test{job="testjob"}', format: 'time_series', instant: true }], interval: '60s', }; - var response = { - status: 'success', - data: { - data: { - resultType: 'vector', - result: [ - { - metric: { __name__: 'test', job: 'testjob' }, - value: [123, '3846'], - }, - ], - }, - }, - }; + beforeEach(async () => { + let response = { + status: 'success', + data: { + data: { + resultType: 'vector', + result: [ + { + metric: { __name__: 'test', job: 'testjob' }, + value: [123, '3846'], + }, + ], + }, + }, + }; + backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response)); ctx.ds = new PrometheusDatasource(instanceSettings, $q, backendSrv, templateSrv, timeSrv); @@ -206,10 +204,7 @@ describe('PrometheusDatasource', function() { }); describe('When performing annotationQuery', function() { var results; - // var urlExpected = - // 'proxied/api/v1/query_range?query=' + - // encodeURIComponent('ALERTS{alertstate="firing"}') + - // '&start=60&end=180&step=60'; + var options = { annotation: { expr: 'ALERTS{alertstate="firing"}', @@ -222,27 +217,29 @@ describe('PrometheusDatasource', function() { to: time({ seconds: 123 }), }, }; - var response = { - status: 'success', - data: { - data: { - resultType: 'matrix', - result: [ - { - metric: { - __name__: 'ALERTS', - alertname: 'InstanceDown', - alertstate: 'firing', - instance: 'testinstance', - job: 'testjob', - }, - values: [[123, '1']], - }, - ], - }, - }, - }; + beforeEach(async () => { + let response = { + status: 'success', + data: { + data: { + resultType: 'matrix', + result: [ + { + metric: { + __name__: 'ALERTS', + alertname: 'InstanceDown', + alertstate: 'firing', + instance: 'testinstance', + job: 'testjob', + }, + values: [[123, '1']], + }, + ], + }, + }, + }; + backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response)); ctx.ds = new PrometheusDatasource(instanceSettings, $q, backendSrv, templateSrv, timeSrv); @@ -262,28 +259,29 @@ describe('PrometheusDatasource', function() { describe('When resultFormat is table and instant = true', function() { var results; - var urlExpected = 'proxied/api/v1/query?query=' + encodeURIComponent('test{job="testjob"}') + '&time=123'; + // var urlExpected = 'proxied/api/v1/query?query=' + encodeURIComponent('test{job="testjob"}') + '&time=123'; var query = { range: { from: time({ seconds: 63 }), to: time({ seconds: 123 }) }, targets: [{ expr: 'test{job="testjob"}', format: 'time_series', instant: true }], interval: '60s', }; - var response = { - status: 'success', - data: { - data: { - resultType: 'vector', - result: [ - { - metric: { __name__: 'test', job: 'testjob' }, - value: [123, '3846'], - }, - ], - }, - }, - }; beforeEach(async () => { + let response = { + status: 'success', + data: { + data: { + resultType: 'vector', + result: [ + { + metric: { __name__: 'test', job: 'testjob' }, + value: [123, '3846'], + }, + ], + }, + }, + }; + backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response)); ctx.ds = new PrometheusDatasource(instanceSettings, $q, backendSrv, templateSrv, timeSrv); await ctx.ds.query(query).then(function(data) { @@ -520,9 +518,13 @@ describe('PrometheusDatasource', function() { __interval_ms: { text: 10 * 1000, value: 10 * 1000 }, }, }; - var urlExpected = - 'proxied/api/v1/query_range?query=' + encodeURIComponent('rate(test[10s])') + '&start=60&end=420&step=10'; + var urlExpected = + 'proxied/api/v1/query_range?query=' + + encodeURIComponent('rate(test[$__interval])') + + '&start=60&end=420&step=10'; + + templateSrv.replace = jest.fn(str => str); backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response)); ctx.ds = new PrometheusDatasource(instanceSettings, $q, backendSrv, templateSrv, timeSrv); await ctx.ds.query(query); @@ -530,10 +532,16 @@ describe('PrometheusDatasource', function() { expect(res.method).toBe('GET'); expect(res.url).toBe(urlExpected); - expect(query.scopedVars.__interval.text).toBe('10s'); - expect(query.scopedVars.__interval.value).toBe('10s'); - expect(query.scopedVars.__interval_ms.text).toBe(10 * 1000); - expect(query.scopedVars.__interval_ms.value).toBe(10 * 1000); + expect(templateSrv.replace.mock.calls[0][1]).toEqual({ + __interval: { + text: '10s', + value: '10s', + }, + __interval_ms: { + text: 10000, + value: 10000, + }, + }); }); it('should be min interval when it is greater than auto interval', async () => { var query = { @@ -552,18 +560,27 @@ describe('PrometheusDatasource', function() { }, }; var urlExpected = - 'proxied/api/v1/query_range?query=' + encodeURIComponent('rate(test[10s])') + '&start=60&end=420&step=10'; + 'proxied/api/v1/query_range?query=' + + encodeURIComponent('rate(test[$__interval])') + + '&start=60&end=420&step=10'; backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response)); + templateSrv.replace = jest.fn(str => str); ctx.ds = new PrometheusDatasource(instanceSettings, $q, backendSrv, templateSrv, timeSrv); await ctx.ds.query(query); let res = backendSrv.datasourceRequest.mock.calls[0][0]; expect(res.method).toBe('GET'); expect(res.url).toBe(urlExpected); - expect(query.scopedVars.__interval.text).toBe('5s'); - expect(query.scopedVars.__interval.value).toBe('5s'); - expect(query.scopedVars.__interval_ms.text).toBe(5 * 1000); - expect(query.scopedVars.__interval_ms.value).toBe(5 * 1000); + expect(templateSrv.replace.mock.calls[0][1]).toEqual({ + __interval: { + text: '5s', + value: '5s', + }, + __interval_ms: { + text: 5000, + value: 5000, + }, + }); }); it('should account for intervalFactor', async () => { var query = { @@ -583,14 +600,28 @@ describe('PrometheusDatasource', function() { }, }; var urlExpected = - 'proxied/api/v1/query_range?query=' + encodeURIComponent('rate(test[100s])') + '&start=0&end=500&step=100'; + 'proxied/api/v1/query_range?query=' + + encodeURIComponent('rate(test[$__interval])') + + '&start=0&end=500&step=100'; backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response)); + templateSrv.replace = jest.fn(str => str); ctx.ds = new PrometheusDatasource(instanceSettings, $q, backendSrv, templateSrv, timeSrv); await ctx.ds.query(query); let res = backendSrv.datasourceRequest.mock.calls[0][0]; expect(res.method).toBe('GET'); expect(res.url).toBe(urlExpected); + expect(templateSrv.replace.mock.calls[0][1]).toEqual({ + __interval: { + text: '10s', + value: '10s', + }, + __interval_ms: { + text: 10000, + value: 10000, + }, + }); + expect(query.scopedVars.__interval.text).toBe('10s'); expect(query.scopedVars.__interval.value).toBe('10s'); expect(query.scopedVars.__interval_ms.text).toBe(10 * 1000); @@ -614,7 +645,11 @@ describe('PrometheusDatasource', function() { }, }; var urlExpected = - 'proxied/api/v1/query_range?query=' + encodeURIComponent('rate(test[50s])') + '&start=50&end=450&step=50'; + 'proxied/api/v1/query_range?query=' + + encodeURIComponent('rate(test[$__interval])') + + '&start=50&end=450&step=50'; + + templateSrv.replace = jest.fn(str => str); backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response)); ctx.ds = new PrometheusDatasource(instanceSettings, $q, backendSrv, templateSrv, timeSrv); await ctx.ds.query(query); @@ -622,10 +657,16 @@ describe('PrometheusDatasource', function() { expect(res.method).toBe('GET'); expect(res.url).toBe(urlExpected); - expect(query.scopedVars.__interval.text).toBe('5s'); - expect(query.scopedVars.__interval.value).toBe('5s'); - expect(query.scopedVars.__interval_ms.text).toBe(5 * 1000); - expect(query.scopedVars.__interval_ms.value).toBe(5 * 1000); + expect(templateSrv.replace.mock.calls[0][1]).toEqual({ + __interval: { + text: '5s', + value: '5s', + }, + __interval_ms: { + text: 5000, + value: 5000, + }, + }); }); it('should be min interval when greater than interval * intervalFactor', async () => { var query = { @@ -645,7 +686,9 @@ describe('PrometheusDatasource', function() { }, }; var urlExpected = - 'proxied/api/v1/query_range?query=' + encodeURIComponent('rate(test[15s])') + '&start=60&end=420&step=15'; + 'proxied/api/v1/query_range?query=' + + encodeURIComponent('rate(test[$__interval])') + + '&start=60&end=420&step=15'; backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response)); ctx.ds = new PrometheusDatasource(instanceSettings, $q, backendSrv, templateSrv, timeSrv); @@ -654,10 +697,16 @@ describe('PrometheusDatasource', function() { expect(res.method).toBe('GET'); expect(res.url).toBe(urlExpected); - expect(query.scopedVars.__interval.text).toBe('5s'); - expect(query.scopedVars.__interval.value).toBe('5s'); - expect(query.scopedVars.__interval_ms.text).toBe(5 * 1000); - expect(query.scopedVars.__interval_ms.value).toBe(5 * 1000); + expect(templateSrv.replace.mock.calls[0][1]).toEqual({ + __interval: { + text: '5s', + value: '5s', + }, + __interval_ms: { + text: 5000, + value: 5000, + }, + }); }); it('should be determined by the 11000 data points limit, accounting for intervalFactor', async () => { var query = { @@ -679,23 +728,30 @@ describe('PrometheusDatasource', function() { var start = 0; var urlExpected = 'proxied/api/v1/query_range?query=' + - encodeURIComponent('rate(test[60s])') + + encodeURIComponent('rate(test[$__interval])') + '&start=' + start + '&end=' + end + '&step=60'; backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response)); + templateSrv.replace = jest.fn(str => str); ctx.ds = new PrometheusDatasource(instanceSettings, $q, backendSrv, templateSrv, timeSrv); await ctx.ds.query(query); let res = backendSrv.datasourceRequest.mock.calls[0][0]; expect(res.method).toBe('GET'); expect(res.url).toBe(urlExpected); - expect(query.scopedVars.__interval.text).toBe('5s'); - expect(query.scopedVars.__interval.value).toBe('5s'); - expect(query.scopedVars.__interval_ms.text).toBe(5 * 1000); - expect(query.scopedVars.__interval_ms.value).toBe(5 * 1000); + expect(templateSrv.replace.mock.calls[0][1]).toEqual({ + __interval: { + text: '5s', + value: '5s', + }, + __interval_ms: { + text: 5000, + value: 5000, + }, + }); }); }); }); @@ -738,21 +794,22 @@ describe('PrometheusDatasource for POST', function() { targets: [{ expr: 'test{job="testjob"}', format: 'time_series' }], interval: '60s', }; - var response = { - status: 'success', - data: { - data: { - resultType: 'matrix', - result: [ - { - metric: { __name__: 'test', job: 'testjob' }, - values: [[2 * 60, '3846']], - }, - ], - }, - }, - }; + beforeEach(async () => { + let response = { + status: 'success', + data: { + data: { + resultType: 'matrix', + result: [ + { + metric: { __name__: 'test', job: 'testjob' }, + values: [[2 * 60, '3846']], + }, + ], + }, + }, + }; backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response)); ctx.ds = new PrometheusDatasource(instanceSettings, $q, backendSrv, templateSrv, timeSrv); await ctx.ds.query(query).then(function(data) { diff --git a/public/app/plugins/datasource/prometheus/specs/datasource_specs.ts b/public/app/plugins/datasource/prometheus/specs/datasource_specs.ts deleted file mode 100644 index c5da671b757..00000000000 --- a/public/app/plugins/datasource/prometheus/specs/datasource_specs.ts +++ /dev/null @@ -1,683 +0,0 @@ -import { describe, beforeEach, it, expect, angularMocks } from 'test/lib/common'; -import moment from 'moment'; -import $ from 'jquery'; -import helpers from 'test/specs/helpers'; -import { PrometheusDatasource } from '../datasource'; - -const SECOND = 1000; -const MINUTE = 60 * SECOND; -const HOUR = 60 * MINUTE; - -const time = ({ hours = 0, seconds = 0, minutes = 0 }) => moment(hours * HOUR + minutes * MINUTE + seconds * SECOND); - -describe('PrometheusDatasource', function() { - var ctx = new helpers.ServiceTestContext(); - var instanceSettings = { - url: 'proxied', - directUrl: 'direct', - user: 'test', - password: 'mupp', - jsonData: { httpMethod: 'GET' }, - }; - - beforeEach(angularMocks.module('grafana.core')); - beforeEach(angularMocks.module('grafana.services')); - beforeEach(ctx.providePhase(['timeSrv'])); - - beforeEach( - angularMocks.inject(function($q, $rootScope, $httpBackend, $injector) { - ctx.$q = $q; - ctx.$httpBackend = $httpBackend; - ctx.$rootScope = $rootScope; - ctx.ds = $injector.instantiate(PrometheusDatasource, { - instanceSettings: instanceSettings, - }); - $httpBackend.when('GET', /\.html$/).respond(''); - }) - ); - describe('When querying prometheus with one target using query editor target spec', function() { - var results; - var query = { - range: { from: time({ seconds: 63 }), to: time({ seconds: 183 }) }, - targets: [{ expr: 'test{job="testjob"}', format: 'time_series' }], - interval: '60s', - }; - // Interval alignment with step - var urlExpected = - 'proxied/api/v1/query_range?query=' + encodeURIComponent('test{job="testjob"}') + '&start=60&end=240&step=60'; - var response = { - status: 'success', - data: { - resultType: 'matrix', - result: [ - { - metric: { __name__: 'test', job: 'testjob' }, - values: [[60, '3846']], - }, - ], - }, - }; - beforeEach(function() { - ctx.$httpBackend.expect('GET', urlExpected).respond(response); - ctx.ds.query(query).then(function(data) { - results = data; - }); - ctx.$httpBackend.flush(); - }); - it('should generate the correct query', function() { - ctx.$httpBackend.verifyNoOutstandingExpectation(); - }); - it('should return series list', function() { - expect(results.data.length).to.be(1); - expect(results.data[0].target).to.be('test{job="testjob"}'); - }); - }); - describe('When querying prometheus with one target which return multiple series', function() { - var results; - var start = 60; - var end = 360; - var step = 60; - var urlExpected = - 'proxied/api/v1/query_range?query=' + - encodeURIComponent('test{job="testjob"}') + - '&start=' + - start + - '&end=' + - end + - '&step=' + - step; - var query = { - range: { from: time({ seconds: start }), to: time({ seconds: end }) }, - targets: [{ expr: 'test{job="testjob"}', format: 'time_series' }], - interval: '60s', - }; - var response = { - status: 'success', - data: { - resultType: 'matrix', - result: [ - { - metric: { __name__: 'test', job: 'testjob', series: 'series 1' }, - values: [[start + step * 1, '3846'], [start + step * 3, '3847'], [end - step * 1, '3848']], - }, - { - metric: { __name__: 'test', job: 'testjob', series: 'series 2' }, - values: [[start + step * 2, '4846']], - }, - ], - }, - }; - beforeEach(function() { - ctx.$httpBackend.expect('GET', urlExpected).respond(response); - ctx.ds.query(query).then(function(data) { - results = data; - }); - ctx.$httpBackend.flush(); - }); - it('should be same length', function() { - expect(results.data.length).to.be(2); - expect(results.data[0].datapoints.length).to.be((end - start) / step + 1); - expect(results.data[1].datapoints.length).to.be((end - start) / step + 1); - }); - it('should fill null until first datapoint in response', function() { - expect(results.data[0].datapoints[0][1]).to.be(start * 1000); - expect(results.data[0].datapoints[0][0]).to.be(null); - expect(results.data[0].datapoints[1][1]).to.be((start + step * 1) * 1000); - expect(results.data[0].datapoints[1][0]).to.be(3846); - }); - it('should fill null after last datapoint in response', function() { - var length = (end - start) / step + 1; - expect(results.data[0].datapoints[length - 2][1]).to.be((end - step * 1) * 1000); - expect(results.data[0].datapoints[length - 2][0]).to.be(3848); - expect(results.data[0].datapoints[length - 1][1]).to.be(end * 1000); - expect(results.data[0].datapoints[length - 1][0]).to.be(null); - }); - it('should fill null at gap between series', function() { - expect(results.data[0].datapoints[2][1]).to.be((start + step * 2) * 1000); - expect(results.data[0].datapoints[2][0]).to.be(null); - expect(results.data[1].datapoints[1][1]).to.be((start + step * 1) * 1000); - expect(results.data[1].datapoints[1][0]).to.be(null); - expect(results.data[1].datapoints[3][1]).to.be((start + step * 3) * 1000); - expect(results.data[1].datapoints[3][0]).to.be(null); - }); - }); - describe('When querying prometheus with one target and instant = true', function() { - var results; - var urlExpected = 'proxied/api/v1/query?query=' + encodeURIComponent('test{job="testjob"}') + '&time=123'; - var query = { - range: { from: time({ seconds: 63 }), to: time({ seconds: 123 }) }, - targets: [{ expr: 'test{job="testjob"}', format: 'time_series', instant: true }], - interval: '60s', - }; - var response = { - status: 'success', - data: { - resultType: 'vector', - result: [ - { - metric: { __name__: 'test', job: 'testjob' }, - value: [123, '3846'], - }, - ], - }, - }; - beforeEach(function() { - ctx.$httpBackend.expect('GET', urlExpected).respond(response); - ctx.ds.query(query).then(function(data) { - results = data; - }); - ctx.$httpBackend.flush(); - }); - it('should generate the correct query', function() { - ctx.$httpBackend.verifyNoOutstandingExpectation(); - }); - it('should return series list', function() { - expect(results.data.length).to.be(1); - expect(results.data[0].target).to.be('test{job="testjob"}'); - }); - }); - describe('When performing annotationQuery', function() { - var results; - var urlExpected = - 'proxied/api/v1/query_range?query=' + - encodeURIComponent('ALERTS{alertstate="firing"}') + - '&start=60&end=180&step=60'; - var options = { - annotation: { - expr: 'ALERTS{alertstate="firing"}', - tagKeys: 'job', - titleFormat: '{{alertname}}', - textFormat: '{{instance}}', - }, - range: { - from: time({ seconds: 63 }), - to: time({ seconds: 123 }), - }, - }; - var response = { - status: 'success', - data: { - resultType: 'matrix', - result: [ - { - metric: { - __name__: 'ALERTS', - alertname: 'InstanceDown', - alertstate: 'firing', - instance: 'testinstance', - job: 'testjob', - }, - values: [[123, '1']], - }, - ], - }, - }; - beforeEach(function() { - ctx.$httpBackend.expect('GET', urlExpected).respond(response); - ctx.ds.annotationQuery(options).then(function(data) { - results = data; - }); - ctx.$httpBackend.flush(); - }); - it('should return annotation list', function() { - ctx.$rootScope.$apply(); - expect(results.length).to.be(1); - expect(results[0].tags).to.contain('testjob'); - expect(results[0].title).to.be('InstanceDown'); - expect(results[0].text).to.be('testinstance'); - expect(results[0].time).to.be(123 * 1000); - }); - }); - - describe('When resultFormat is table and instant = true', function() { - var results; - var urlExpected = 'proxied/api/v1/query?query=' + encodeURIComponent('test{job="testjob"}') + '&time=123'; - var query = { - range: { from: time({ seconds: 63 }), to: time({ seconds: 123 }) }, - targets: [{ expr: 'test{job="testjob"}', format: 'time_series', instant: true }], - interval: '60s', - }; - var response = { - status: 'success', - data: { - resultType: 'vector', - result: [ - { - metric: { __name__: 'test', job: 'testjob' }, - value: [123, '3846'], - }, - ], - }, - }; - - beforeEach(function() { - ctx.$httpBackend.expect('GET', urlExpected).respond(response); - ctx.ds.query(query).then(function(data) { - results = data; - }); - ctx.$httpBackend.flush(); - }); - - it('should return result', () => { - expect(results).not.to.be(null); - }); - }); - - describe('The "step" query parameter', function() { - var response = { - status: 'success', - data: { - resultType: 'matrix', - result: [], - }, - }; - - it('should be min interval when greater than auto interval', function() { - var query = { - // 6 minute range - range: { from: time({ minutes: 1 }), to: time({ minutes: 7 }) }, - targets: [ - { - expr: 'test', - interval: '10s', - }, - ], - interval: '5s', - }; - var urlExpected = 'proxied/api/v1/query_range?query=test&start=60&end=420&step=10'; - ctx.$httpBackend.expect('GET', urlExpected).respond(response); - ctx.ds.query(query); - ctx.$httpBackend.verifyNoOutstandingExpectation(); - }); - - it('step should never go below 1', function() { - var query = { - // 6 minute range - range: { from: time({ minutes: 1 }), to: time({ minutes: 7 }) }, - targets: [{ expr: 'test' }], - interval: '100ms', - }; - var urlExpected = 'proxied/api/v1/query_range?query=test&start=60&end=420&step=1'; - ctx.$httpBackend.expect('GET', urlExpected).respond(response); - ctx.ds.query(query); - ctx.$httpBackend.verifyNoOutstandingExpectation(); - }); - - it('should be auto interval when greater than min interval', function() { - var query = { - // 6 minute range - range: { from: time({ minutes: 1 }), to: time({ minutes: 7 }) }, - targets: [ - { - expr: 'test', - interval: '5s', - }, - ], - interval: '10s', - }; - var urlExpected = 'proxied/api/v1/query_range?query=test&start=60&end=420&step=10'; - ctx.$httpBackend.expect('GET', urlExpected).respond(response); - ctx.ds.query(query); - ctx.$httpBackend.verifyNoOutstandingExpectation(); - }); - it('should result in querying fewer than 11000 data points', function() { - var query = { - // 6 hour range - range: { from: time({ hours: 1 }), to: time({ hours: 7 }) }, - targets: [{ expr: 'test' }], - interval: '1s', - }; - var end = 7 * 60 * 60; - var start = 60 * 60; - var urlExpected = 'proxied/api/v1/query_range?query=test&start=' + start + '&end=' + end + '&step=2'; - ctx.$httpBackend.expect('GET', urlExpected).respond(response); - ctx.ds.query(query); - ctx.$httpBackend.verifyNoOutstandingExpectation(); - }); - it('should not apply min interval when interval * intervalFactor greater', function() { - var query = { - // 6 minute range - range: { from: time({ minutes: 1 }), to: time({ minutes: 7 }) }, - targets: [ - { - expr: 'test', - interval: '10s', - intervalFactor: 10, - }, - ], - interval: '5s', - }; - // times get rounded up to interval - var urlExpected = 'proxied/api/v1/query_range?query=test&start=50&end=450&step=50'; - ctx.$httpBackend.expect('GET', urlExpected).respond(response); - ctx.ds.query(query); - ctx.$httpBackend.verifyNoOutstandingExpectation(); - }); - it('should apply min interval when interval * intervalFactor smaller', function() { - var query = { - // 6 minute range - range: { from: time({ minutes: 1 }), to: time({ minutes: 7 }) }, - targets: [ - { - expr: 'test', - interval: '15s', - intervalFactor: 2, - }, - ], - interval: '5s', - }; - var urlExpected = 'proxied/api/v1/query_range?query=test' + '&start=60&end=420&step=15'; - ctx.$httpBackend.expect('GET', urlExpected).respond(response); - ctx.ds.query(query); - ctx.$httpBackend.verifyNoOutstandingExpectation(); - }); - it('should apply intervalFactor to auto interval when greater', function() { - var query = { - // 6 minute range - range: { from: time({ minutes: 1 }), to: time({ minutes: 7 }) }, - targets: [ - { - expr: 'test', - interval: '5s', - intervalFactor: 10, - }, - ], - interval: '10s', - }; - // times get aligned to interval - var urlExpected = 'proxied/api/v1/query_range?query=test' + '&start=0&end=500&step=100'; - ctx.$httpBackend.expect('GET', urlExpected).respond(response); - ctx.ds.query(query); - ctx.$httpBackend.verifyNoOutstandingExpectation(); - }); - it('should not not be affected by the 11000 data points limit when large enough', function() { - var query = { - // 1 week range - range: { from: time({}), to: time({ hours: 7 * 24 }) }, - targets: [ - { - expr: 'test', - intervalFactor: 10, - }, - ], - interval: '10s', - }; - var end = 7 * 24 * 60 * 60; - var start = 0; - var urlExpected = 'proxied/api/v1/query_range?query=test' + '&start=' + start + '&end=' + end + '&step=100'; - ctx.$httpBackend.expect('GET', urlExpected).respond(response); - ctx.ds.query(query); - ctx.$httpBackend.verifyNoOutstandingExpectation(); - }); - it('should be determined by the 11000 data points limit when too small', function() { - var query = { - // 1 week range - range: { from: time({}), to: time({ hours: 7 * 24 }) }, - targets: [ - { - expr: 'test', - intervalFactor: 10, - }, - ], - interval: '5s', - }; - var end = 7 * 24 * 60 * 60; - var start = 0; - var urlExpected = 'proxied/api/v1/query_range?query=test' + '&start=' + start + '&end=' + end + '&step=60'; - ctx.$httpBackend.expect('GET', urlExpected).respond(response); - ctx.ds.query(query); - ctx.$httpBackend.verifyNoOutstandingExpectation(); - }); - }); - - describe('The __interval and __interval_ms template variables', function() { - var response = { - status: 'success', - data: { - resultType: 'matrix', - result: [], - }, - }; - - it('should be unchanged when auto interval is greater than min interval', function() { - var query = { - // 6 minute range - range: { from: time({ minutes: 1 }), to: time({ minutes: 7 }) }, - targets: [ - { - expr: 'rate(test[$__interval])', - interval: '5s', - }, - ], - interval: '10s', - scopedVars: { - __interval: { text: '10s', value: '10s' }, - __interval_ms: { text: 10 * 1000, value: 10 * 1000 }, - }, - }; - var urlExpected = - 'proxied/api/v1/query_range?query=' + encodeURIComponent('rate(test[10s])') + '&start=60&end=420&step=10'; - ctx.$httpBackend.expect('GET', urlExpected).respond(response); - ctx.ds.query(query); - ctx.$httpBackend.verifyNoOutstandingExpectation(); - - expect(query.scopedVars.__interval.text).to.be('10s'); - expect(query.scopedVars.__interval.value).to.be('10s'); - expect(query.scopedVars.__interval_ms.text).to.be(10 * 1000); - expect(query.scopedVars.__interval_ms.value).to.be(10 * 1000); - }); - it('should be min interval when it is greater than auto interval', function() { - var query = { - // 6 minute range - range: { from: time({ minutes: 1 }), to: time({ minutes: 7 }) }, - targets: [ - { - expr: 'rate(test[$__interval])', - interval: '10s', - }, - ], - interval: '5s', - scopedVars: { - __interval: { text: '5s', value: '5s' }, - __interval_ms: { text: 5 * 1000, value: 5 * 1000 }, - }, - }; - var urlExpected = - 'proxied/api/v1/query_range?query=' + encodeURIComponent('rate(test[10s])') + '&start=60&end=420&step=10'; - ctx.$httpBackend.expect('GET', urlExpected).respond(response); - ctx.ds.query(query); - ctx.$httpBackend.verifyNoOutstandingExpectation(); - - expect(query.scopedVars.__interval.text).to.be('5s'); - expect(query.scopedVars.__interval.value).to.be('5s'); - expect(query.scopedVars.__interval_ms.text).to.be(5 * 1000); - expect(query.scopedVars.__interval_ms.value).to.be(5 * 1000); - }); - it('should account for intervalFactor', function() { - var query = { - // 6 minute range - range: { from: time({ minutes: 1 }), to: time({ minutes: 7 }) }, - targets: [ - { - expr: 'rate(test[$__interval])', - interval: '5s', - intervalFactor: 10, - }, - ], - interval: '10s', - scopedVars: { - __interval: { text: '10s', value: '10s' }, - __interval_ms: { text: 10 * 1000, value: 10 * 1000 }, - }, - }; - var urlExpected = - 'proxied/api/v1/query_range?query=' + encodeURIComponent('rate(test[100s])') + '&start=0&end=500&step=100'; - ctx.$httpBackend.expect('GET', urlExpected).respond(response); - ctx.ds.query(query); - ctx.$httpBackend.verifyNoOutstandingExpectation(); - - expect(query.scopedVars.__interval.text).to.be('10s'); - expect(query.scopedVars.__interval.value).to.be('10s'); - expect(query.scopedVars.__interval_ms.text).to.be(10 * 1000); - expect(query.scopedVars.__interval_ms.value).to.be(10 * 1000); - }); - it('should be interval * intervalFactor when greater than min interval', function() { - var query = { - // 6 minute range - range: { from: time({ minutes: 1 }), to: time({ minutes: 7 }) }, - targets: [ - { - expr: 'rate(test[$__interval])', - interval: '10s', - intervalFactor: 10, - }, - ], - interval: '5s', - scopedVars: { - __interval: { text: '5s', value: '5s' }, - __interval_ms: { text: 5 * 1000, value: 5 * 1000 }, - }, - }; - var urlExpected = - 'proxied/api/v1/query_range?query=' + encodeURIComponent('rate(test[50s])') + '&start=50&end=450&step=50'; - ctx.$httpBackend.expect('GET', urlExpected).respond(response); - ctx.ds.query(query); - ctx.$httpBackend.verifyNoOutstandingExpectation(); - - expect(query.scopedVars.__interval.text).to.be('5s'); - expect(query.scopedVars.__interval.value).to.be('5s'); - expect(query.scopedVars.__interval_ms.text).to.be(5 * 1000); - expect(query.scopedVars.__interval_ms.value).to.be(5 * 1000); - }); - it('should be min interval when greater than interval * intervalFactor', function() { - var query = { - // 6 minute range - range: { from: time({ minutes: 1 }), to: time({ minutes: 7 }) }, - targets: [ - { - expr: 'rate(test[$__interval])', - interval: '15s', - intervalFactor: 2, - }, - ], - interval: '5s', - scopedVars: { - __interval: { text: '5s', value: '5s' }, - __interval_ms: { text: 5 * 1000, value: 5 * 1000 }, - }, - }; - var urlExpected = - 'proxied/api/v1/query_range?query=' + encodeURIComponent('rate(test[15s])') + '&start=60&end=420&step=15'; - ctx.$httpBackend.expect('GET', urlExpected).respond(response); - ctx.ds.query(query); - ctx.$httpBackend.verifyNoOutstandingExpectation(); - - expect(query.scopedVars.__interval.text).to.be('5s'); - expect(query.scopedVars.__interval.value).to.be('5s'); - expect(query.scopedVars.__interval_ms.text).to.be(5 * 1000); - expect(query.scopedVars.__interval_ms.value).to.be(5 * 1000); - }); - it('should be determined by the 11000 data points limit, accounting for intervalFactor', function() { - var query = { - // 1 week range - range: { from: time({}), to: time({ hours: 7 * 24 }) }, - targets: [ - { - expr: 'rate(test[$__interval])', - intervalFactor: 10, - }, - ], - interval: '5s', - scopedVars: { - __interval: { text: '5s', value: '5s' }, - __interval_ms: { text: 5 * 1000, value: 5 * 1000 }, - }, - }; - var end = 7 * 24 * 60 * 60; - var start = 0; - var urlExpected = - 'proxied/api/v1/query_range?query=' + - encodeURIComponent('rate(test[60s])') + - '&start=' + - start + - '&end=' + - end + - '&step=60'; - ctx.$httpBackend.expect('GET', urlExpected).respond(response); - ctx.ds.query(query); - ctx.$httpBackend.verifyNoOutstandingExpectation(); - - expect(query.scopedVars.__interval.text).to.be('5s'); - expect(query.scopedVars.__interval.value).to.be('5s'); - expect(query.scopedVars.__interval_ms.text).to.be(5 * 1000); - expect(query.scopedVars.__interval_ms.value).to.be(5 * 1000); - }); - }); -}); - -describe('PrometheusDatasource for POST', function() { - var ctx = new helpers.ServiceTestContext(); - var instanceSettings = { - url: 'proxied', - directUrl: 'direct', - user: 'test', - password: 'mupp', - jsonData: { httpMethod: 'POST' }, - }; - - beforeEach(angularMocks.module('grafana.core')); - beforeEach(angularMocks.module('grafana.services')); - beforeEach(ctx.providePhase(['timeSrv'])); - - beforeEach( - angularMocks.inject(function($q, $rootScope, $httpBackend, $injector) { - ctx.$q = $q; - ctx.$httpBackend = $httpBackend; - ctx.$rootScope = $rootScope; - ctx.ds = $injector.instantiate(PrometheusDatasource, { instanceSettings: instanceSettings }); - $httpBackend.when('GET', /\.html$/).respond(''); - }) - ); - - describe('When querying prometheus with one target using query editor target spec', function() { - var results; - var urlExpected = 'proxied/api/v1/query_range'; - var dataExpected = $.param({ - query: 'test{job="testjob"}', - start: 1 * 60, - end: 3 * 60, - step: 60, - }); - var query = { - range: { from: time({ minutes: 1, seconds: 3 }), to: time({ minutes: 2, seconds: 3 }) }, - targets: [{ expr: 'test{job="testjob"}', format: 'time_series' }], - interval: '60s', - }; - var response = { - status: 'success', - data: { - resultType: 'matrix', - result: [ - { - metric: { __name__: 'test', job: 'testjob' }, - values: [[2 * 60, '3846']], - }, - ], - }, - }; - beforeEach(function() { - ctx.$httpBackend.expectPOST(urlExpected, dataExpected).respond(response); - ctx.ds.query(query).then(function(data) { - results = data; - }); - ctx.$httpBackend.flush(); - }); - it('should generate the correct query', function() { - ctx.$httpBackend.verifyNoOutstandingExpectation(); - }); - it('should return series list', function() { - expect(results.data.length).to.be(1); - expect(results.data[0].target).to.be('test{job="testjob"}'); - }); - }); -}); From 790aadf8ef3544eb0c1007042525c7ad54f611e2 Mon Sep 17 00:00:00 2001 From: Tobias Skarhed Date: Wed, 1 Aug 2018 10:09:05 +0200 Subject: [PATCH 238/786] Remove angularMocks --- .../app/plugins/datasource/prometheus/specs/_datasource.jest.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/public/app/plugins/datasource/prometheus/specs/_datasource.jest.ts b/public/app/plugins/datasource/prometheus/specs/_datasource.jest.ts index 2deab13a101..efe2738cce9 100644 --- a/public/app/plugins/datasource/prometheus/specs/_datasource.jest.ts +++ b/public/app/plugins/datasource/prometheus/specs/_datasource.jest.ts @@ -1,7 +1,6 @@ import moment from 'moment'; import { PrometheusDatasource } from '../datasource'; import $q from 'q'; -import { angularMocks } from 'test/lib/common'; const SECOND = 1000; const MINUTE = 60 * SECOND; From 8d0c4cdc09c04a05f20d3988380613a3f9f1e87f Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Wed, 1 Aug 2018 12:30:50 +0200 Subject: [PATCH 239/786] changelog: add notes about closing #12561 [skip ci] --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index dde7ead6f13..aa089b5900b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,7 @@ * **Cloudwatch**: Improved error handling [#12489](https://github.com/grafana/grafana/issues/12489), thx [@mtanda](https://github.com/mtanda) * **Cloudwatch**: AppSync metrics and dimensions [#12300](https://github.com/grafana/grafana/issues/12300), thx [@franciscocpg](https://github.com/franciscocpg) * **Cloudwatch**: Direct Connect metrics and dimensions [#12762](https://github.com/grafana/grafana/pulls/12762), thx [@mindriot88](https://github.com/mindriot88) +* **Cloudwatch**: Added BurstBalance metric to list of AWS RDS metrics [#12561](https://github.com/grafana/grafana/pulls/12561), thx [@activeshadow](https://github.com/activeshadow) * **Table**: Adjust header contrast for the light theme [#12668](https://github.com/grafana/grafana/issues/12668) * **Elasticsearch**: For alerting/backend, support having index name to the right of pattern in index pattern [#12731](https://github.com/grafana/grafana/issues/12731) * **OAuth**: Fix overriding tls_skip_verify_insecure using environment variable [#12747](https://github.com/grafana/grafana/issues/12747), thx [@jangaraj](https://github.com/jangaraj) From af32bfebefcc02170fbaa4104ae2e5883b5c1ba8 Mon Sep 17 00:00:00 2001 From: Tobias Skarhed Date: Wed, 1 Aug 2018 14:26:29 +0200 Subject: [PATCH 240/786] Add all tests to one file --- .../prometheus/specs/_datasource.jest.ts | 829 ------------------ .../prometheus/specs/datasource.jest.ts | 794 +++++++++++++++++ 2 files changed, 794 insertions(+), 829 deletions(-) delete mode 100644 public/app/plugins/datasource/prometheus/specs/_datasource.jest.ts diff --git a/public/app/plugins/datasource/prometheus/specs/_datasource.jest.ts b/public/app/plugins/datasource/prometheus/specs/_datasource.jest.ts deleted file mode 100644 index efe2738cce9..00000000000 --- a/public/app/plugins/datasource/prometheus/specs/_datasource.jest.ts +++ /dev/null @@ -1,829 +0,0 @@ -import moment from 'moment'; -import { PrometheusDatasource } from '../datasource'; -import $q from 'q'; - -const SECOND = 1000; -const MINUTE = 60 * SECOND; -const HOUR = 60 * MINUTE; - -const time = ({ hours = 0, seconds = 0, minutes = 0 }) => moment(hours * HOUR + minutes * MINUTE + seconds * SECOND); - -let ctx = {}; -let instanceSettings = { - url: 'proxied', - directUrl: 'direct', - user: 'test', - password: 'mupp', - jsonData: { httpMethod: 'GET' }, -}; -let backendSrv = { - datasourceRequest: jest.fn(), -}; - -let templateSrv = { - replace: jest.fn(str => str), -}; - -let timeSrv = { - timeRange: () => { - return { to: { diff: () => 2000 }, from: '' }; - }, -}; - -describe('PrometheusDatasource', function() { - // beforeEach(angularMocks.module('grafana.core')); - // beforeEach(angularMocks.module('grafana.services')); - // beforeEach(ctx.providePhase(['timeSrv'])); - - // beforeEach( - // angularMocks.inject(function($q, $rootScope, $httpBackend, $injector) { - // ctx.$q = $q; - // ctx.$httpBackend = $httpBackend; - // ctx.$rootScope = $rootScope; - // ctx.ds = $injector.instantiate(PrometheusDatasource, { - // instanceSettings: instanceSettings, - // }); - // $httpBackend.when('GET', /\.html$/).respond(''); - // }) - // ); - - describe('When querying prometheus with one target using query editor target spec', async () => { - var results; - var query = { - range: { from: time({ seconds: 63 }), to: time({ seconds: 183 }) }, - targets: [{ expr: 'test{job="testjob"}', format: 'time_series' }], - interval: '60s', - }; - // Interval alignment with step - var urlExpected = - 'proxied/api/v1/query_range?query=' + encodeURIComponent('test{job="testjob"}') + '&start=60&end=240&step=60'; - - beforeEach(async () => { - let response = { - data: { - status: 'success', - data: { - resultType: 'matrix', - result: [ - { - metric: { __name__: 'test', job: 'testjob' }, - values: [[60, '3846']], - }, - ], - }, - }, - }; - backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response)); - ctx.ds = new PrometheusDatasource(instanceSettings, $q, backendSrv, templateSrv, timeSrv); - - await ctx.ds.query(query).then(function(data) { - results = data; - }); - }); - - it('should generate the correct query', function() { - let res = backendSrv.datasourceRequest.mock.calls[0][0]; - expect(res.method).toBe('GET'); - expect(res.url).toBe(urlExpected); - }); - it('should return series list', async () => { - expect(results.data.length).toBe(1); - expect(results.data[0].target).toBe('test{job="testjob"}'); - }); - }); - describe('When querying prometheus with one target which return multiple series', function() { - var results; - var start = 60; - var end = 360; - var step = 60; - - var query = { - range: { from: time({ seconds: start }), to: time({ seconds: end }) }, - targets: [{ expr: 'test{job="testjob"}', format: 'time_series' }], - interval: '60s', - }; - - beforeEach(async () => { - let response = { - status: 'success', - data: { - data: { - resultType: 'matrix', - result: [ - { - metric: { __name__: 'test', job: 'testjob', series: 'series 1' }, - values: [[start + step * 1, '3846'], [start + step * 3, '3847'], [end - step * 1, '3848']], - }, - { - metric: { __name__: 'test', job: 'testjob', series: 'series 2' }, - values: [[start + step * 2, '4846']], - }, - ], - }, - }, - }; - - backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response)); - ctx.ds = new PrometheusDatasource(instanceSettings, $q, backendSrv, templateSrv, timeSrv); - - await ctx.ds.query(query).then(function(data) { - results = data; - }); - }); - - it('should be same length', function() { - expect(results.data.length).toBe(2); - expect(results.data[0].datapoints.length).toBe((end - start) / step + 1); - expect(results.data[1].datapoints.length).toBe((end - start) / step + 1); - }); - - it('should fill null until first datapoint in response', function() { - expect(results.data[0].datapoints[0][1]).toBe(start * 1000); - expect(results.data[0].datapoints[0][0]).toBe(null); - expect(results.data[0].datapoints[1][1]).toBe((start + step * 1) * 1000); - expect(results.data[0].datapoints[1][0]).toBe(3846); - }); - it('should fill null after last datapoint in response', function() { - var length = (end - start) / step + 1; - expect(results.data[0].datapoints[length - 2][1]).toBe((end - step * 1) * 1000); - expect(results.data[0].datapoints[length - 2][0]).toBe(3848); - expect(results.data[0].datapoints[length - 1][1]).toBe(end * 1000); - expect(results.data[0].datapoints[length - 1][0]).toBe(null); - }); - it('should fill null at gap between series', function() { - expect(results.data[0].datapoints[2][1]).toBe((start + step * 2) * 1000); - expect(results.data[0].datapoints[2][0]).toBe(null); - expect(results.data[1].datapoints[1][1]).toBe((start + step * 1) * 1000); - expect(results.data[1].datapoints[1][0]).toBe(null); - expect(results.data[1].datapoints[3][1]).toBe((start + step * 3) * 1000); - expect(results.data[1].datapoints[3][0]).toBe(null); - }); - }); - describe('When querying prometheus with one target and instant = true', function() { - var results; - var urlExpected = 'proxied/api/v1/query?query=' + encodeURIComponent('test{job="testjob"}') + '&time=123'; - var query = { - range: { from: time({ seconds: 63 }), to: time({ seconds: 123 }) }, - targets: [{ expr: 'test{job="testjob"}', format: 'time_series', instant: true }], - interval: '60s', - }; - - beforeEach(async () => { - let response = { - status: 'success', - data: { - data: { - resultType: 'vector', - result: [ - { - metric: { __name__: 'test', job: 'testjob' }, - value: [123, '3846'], - }, - ], - }, - }, - }; - - backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response)); - ctx.ds = new PrometheusDatasource(instanceSettings, $q, backendSrv, templateSrv, timeSrv); - - await ctx.ds.query(query).then(function(data) { - results = data; - }); - }); - it('should generate the correct query', function() { - let res = backendSrv.datasourceRequest.mock.calls[0][0]; - expect(res.method).toBe('GET'); - expect(res.url).toBe(urlExpected); - }); - it('should return series list', function() { - expect(results.data.length).toBe(1); - expect(results.data[0].target).toBe('test{job="testjob"}'); - }); - }); - describe('When performing annotationQuery', function() { - var results; - - var options = { - annotation: { - expr: 'ALERTS{alertstate="firing"}', - tagKeys: 'job', - titleFormat: '{{alertname}}', - textFormat: '{{instance}}', - }, - range: { - from: time({ seconds: 63 }), - to: time({ seconds: 123 }), - }, - }; - - beforeEach(async () => { - let response = { - status: 'success', - data: { - data: { - resultType: 'matrix', - result: [ - { - metric: { - __name__: 'ALERTS', - alertname: 'InstanceDown', - alertstate: 'firing', - instance: 'testinstance', - job: 'testjob', - }, - values: [[123, '1']], - }, - ], - }, - }, - }; - - backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response)); - ctx.ds = new PrometheusDatasource(instanceSettings, $q, backendSrv, templateSrv, timeSrv); - - await ctx.ds.annotationQuery(options).then(function(data) { - results = data; - }); - }); - it('should return annotation list', function() { - // ctx.$rootScope.$apply(); - expect(results.length).toBe(1); - expect(results[0].tags).toContain('testjob'); - expect(results[0].title).toBe('InstanceDown'); - expect(results[0].text).toBe('testinstance'); - expect(results[0].time).toBe(123 * 1000); - }); - }); - - describe('When resultFormat is table and instant = true', function() { - var results; - // var urlExpected = 'proxied/api/v1/query?query=' + encodeURIComponent('test{job="testjob"}') + '&time=123'; - var query = { - range: { from: time({ seconds: 63 }), to: time({ seconds: 123 }) }, - targets: [{ expr: 'test{job="testjob"}', format: 'time_series', instant: true }], - interval: '60s', - }; - - beforeEach(async () => { - let response = { - status: 'success', - data: { - data: { - resultType: 'vector', - result: [ - { - metric: { __name__: 'test', job: 'testjob' }, - value: [123, '3846'], - }, - ], - }, - }, - }; - - backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response)); - ctx.ds = new PrometheusDatasource(instanceSettings, $q, backendSrv, templateSrv, timeSrv); - await ctx.ds.query(query).then(function(data) { - results = data; - }); - }); - - it('should return result', () => { - expect(results).not.toBe(null); - }); - }); - - describe('The "step" query parameter', function() { - var response = { - status: 'success', - data: { - data: { - resultType: 'matrix', - result: [], - }, - }, - }; - - it('should be min interval when greater than auto interval', async () => { - let query = { - // 6 minute range - range: { from: time({ minutes: 1 }), to: time({ minutes: 7 }) }, - targets: [ - { - expr: 'test', - interval: '10s', - }, - ], - interval: '5s', - }; - let urlExpected = 'proxied/api/v1/query_range?query=test&start=60&end=420&step=10'; - - backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response)); - ctx.ds = new PrometheusDatasource(instanceSettings, $q, backendSrv, templateSrv, timeSrv); - await ctx.ds.query(query); - let res = backendSrv.datasourceRequest.mock.calls[0][0]; - expect(res.method).toBe('GET'); - expect(res.url).toBe(urlExpected); - }); - - it('step should never go below 1', async () => { - var query = { - // 6 minute range - range: { from: time({ minutes: 1 }), to: time({ minutes: 7 }) }, - targets: [{ expr: 'test' }], - interval: '100ms', - }; - var urlExpected = 'proxied/api/v1/query_range?query=test&start=60&end=420&step=1'; - backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response)); - ctx.ds = new PrometheusDatasource(instanceSettings, $q, backendSrv, templateSrv, timeSrv); - await ctx.ds.query(query); - let res = backendSrv.datasourceRequest.mock.calls[0][0]; - expect(res.method).toBe('GET'); - expect(res.url).toBe(urlExpected); - }); - - it('should be auto interval when greater than min interval', async () => { - var query = { - // 6 minute range - range: { from: time({ minutes: 1 }), to: time({ minutes: 7 }) }, - targets: [ - { - expr: 'test', - interval: '5s', - }, - ], - interval: '10s', - }; - var urlExpected = 'proxied/api/v1/query_range?query=test&start=60&end=420&step=10'; - backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response)); - ctx.ds = new PrometheusDatasource(instanceSettings, $q, backendSrv, templateSrv, timeSrv); - await ctx.ds.query(query); - let res = backendSrv.datasourceRequest.mock.calls[0][0]; - expect(res.method).toBe('GET'); - expect(res.url).toBe(urlExpected); - }); - it('should result in querying fewer than 11000 data points', async () => { - var query = { - // 6 hour range - range: { from: time({ hours: 1 }), to: time({ hours: 7 }) }, - targets: [{ expr: 'test' }], - interval: '1s', - }; - var end = 7 * 60 * 60; - var start = 60 * 60; - var urlExpected = 'proxied/api/v1/query_range?query=test&start=' + start + '&end=' + end + '&step=2'; - backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response)); - ctx.ds = new PrometheusDatasource(instanceSettings, $q, backendSrv, templateSrv, timeSrv); - await ctx.ds.query(query); - let res = backendSrv.datasourceRequest.mock.calls[0][0]; - expect(res.method).toBe('GET'); - expect(res.url).toBe(urlExpected); - }); - it('should not apply min interval when interval * intervalFactor greater', async () => { - var query = { - // 6 minute range - range: { from: time({ minutes: 1 }), to: time({ minutes: 7 }) }, - targets: [ - { - expr: 'test', - interval: '10s', - intervalFactor: 10, - }, - ], - interval: '5s', - }; - // times get rounded up to interval - var urlExpected = 'proxied/api/v1/query_range?query=test&start=50&end=450&step=50'; - backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response)); - ctx.ds = new PrometheusDatasource(instanceSettings, $q, backendSrv, templateSrv, timeSrv); - await ctx.ds.query(query); - let res = backendSrv.datasourceRequest.mock.calls[0][0]; - expect(res.method).toBe('GET'); - expect(res.url).toBe(urlExpected); - }); - it('should apply min interval when interval * intervalFactor smaller', async () => { - var query = { - // 6 minute range - range: { from: time({ minutes: 1 }), to: time({ minutes: 7 }) }, - targets: [ - { - expr: 'test', - interval: '15s', - intervalFactor: 2, - }, - ], - interval: '5s', - }; - var urlExpected = 'proxied/api/v1/query_range?query=test' + '&start=60&end=420&step=15'; - backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response)); - ctx.ds = new PrometheusDatasource(instanceSettings, $q, backendSrv, templateSrv, timeSrv); - await ctx.ds.query(query); - let res = backendSrv.datasourceRequest.mock.calls[0][0]; - expect(res.method).toBe('GET'); - expect(res.url).toBe(urlExpected); - }); - it('should apply intervalFactor to auto interval when greater', async () => { - var query = { - // 6 minute range - range: { from: time({ minutes: 1 }), to: time({ minutes: 7 }) }, - targets: [ - { - expr: 'test', - interval: '5s', - intervalFactor: 10, - }, - ], - interval: '10s', - }; - // times get aligned to interval - var urlExpected = 'proxied/api/v1/query_range?query=test' + '&start=0&end=500&step=100'; - backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response)); - ctx.ds = new PrometheusDatasource(instanceSettings, $q, backendSrv, templateSrv, timeSrv); - await ctx.ds.query(query); - let res = backendSrv.datasourceRequest.mock.calls[0][0]; - expect(res.method).toBe('GET'); - expect(res.url).toBe(urlExpected); - }); - it('should not not be affected by the 11000 data points limit when large enough', async () => { - var query = { - // 1 week range - range: { from: time({}), to: time({ hours: 7 * 24 }) }, - targets: [ - { - expr: 'test', - intervalFactor: 10, - }, - ], - interval: '10s', - }; - var end = 7 * 24 * 60 * 60; - var start = 0; - var urlExpected = 'proxied/api/v1/query_range?query=test' + '&start=' + start + '&end=' + end + '&step=100'; - backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response)); - ctx.ds = new PrometheusDatasource(instanceSettings, $q, backendSrv, templateSrv, timeSrv); - await ctx.ds.query(query); - let res = backendSrv.datasourceRequest.mock.calls[0][0]; - expect(res.method).toBe('GET'); - expect(res.url).toBe(urlExpected); - }); - it('should be determined by the 11000 data points limit when too small', async () => { - var query = { - // 1 week range - range: { from: time({}), to: time({ hours: 7 * 24 }) }, - targets: [ - { - expr: 'test', - intervalFactor: 10, - }, - ], - interval: '5s', - }; - var end = 7 * 24 * 60 * 60; - var start = 0; - var urlExpected = 'proxied/api/v1/query_range?query=test' + '&start=' + start + '&end=' + end + '&step=60'; - backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response)); - ctx.ds = new PrometheusDatasource(instanceSettings, $q, backendSrv, templateSrv, timeSrv); - await ctx.ds.query(query); - let res = backendSrv.datasourceRequest.mock.calls[0][0]; - expect(res.method).toBe('GET'); - expect(res.url).toBe(urlExpected); - }); - }); - - describe('The __interval and __interval_ms template variables', function() { - var response = { - status: 'success', - data: { - data: { - resultType: 'matrix', - result: [], - }, - }, - }; - - it('should be unchanged when auto interval is greater than min interval', async () => { - var query = { - // 6 minute range - range: { from: time({ minutes: 1 }), to: time({ minutes: 7 }) }, - targets: [ - { - expr: 'rate(test[$__interval])', - interval: '5s', - }, - ], - interval: '10s', - scopedVars: { - __interval: { text: '10s', value: '10s' }, - __interval_ms: { text: 10 * 1000, value: 10 * 1000 }, - }, - }; - - var urlExpected = - 'proxied/api/v1/query_range?query=' + - encodeURIComponent('rate(test[$__interval])') + - '&start=60&end=420&step=10'; - - templateSrv.replace = jest.fn(str => str); - backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response)); - ctx.ds = new PrometheusDatasource(instanceSettings, $q, backendSrv, templateSrv, timeSrv); - await ctx.ds.query(query); - let res = backendSrv.datasourceRequest.mock.calls[0][0]; - expect(res.method).toBe('GET'); - expect(res.url).toBe(urlExpected); - - expect(templateSrv.replace.mock.calls[0][1]).toEqual({ - __interval: { - text: '10s', - value: '10s', - }, - __interval_ms: { - text: 10000, - value: 10000, - }, - }); - }); - it('should be min interval when it is greater than auto interval', async () => { - var query = { - // 6 minute range - range: { from: time({ minutes: 1 }), to: time({ minutes: 7 }) }, - targets: [ - { - expr: 'rate(test[$__interval])', - interval: '10s', - }, - ], - interval: '5s', - scopedVars: { - __interval: { text: '5s', value: '5s' }, - __interval_ms: { text: 5 * 1000, value: 5 * 1000 }, - }, - }; - var urlExpected = - 'proxied/api/v1/query_range?query=' + - encodeURIComponent('rate(test[$__interval])') + - '&start=60&end=420&step=10'; - backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response)); - templateSrv.replace = jest.fn(str => str); - ctx.ds = new PrometheusDatasource(instanceSettings, $q, backendSrv, templateSrv, timeSrv); - await ctx.ds.query(query); - let res = backendSrv.datasourceRequest.mock.calls[0][0]; - expect(res.method).toBe('GET'); - expect(res.url).toBe(urlExpected); - - expect(templateSrv.replace.mock.calls[0][1]).toEqual({ - __interval: { - text: '5s', - value: '5s', - }, - __interval_ms: { - text: 5000, - value: 5000, - }, - }); - }); - it('should account for intervalFactor', async () => { - var query = { - // 6 minute range - range: { from: time({ minutes: 1 }), to: time({ minutes: 7 }) }, - targets: [ - { - expr: 'rate(test[$__interval])', - interval: '5s', - intervalFactor: 10, - }, - ], - interval: '10s', - scopedVars: { - __interval: { text: '10s', value: '10s' }, - __interval_ms: { text: 10 * 1000, value: 10 * 1000 }, - }, - }; - var urlExpected = - 'proxied/api/v1/query_range?query=' + - encodeURIComponent('rate(test[$__interval])') + - '&start=0&end=500&step=100'; - backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response)); - templateSrv.replace = jest.fn(str => str); - ctx.ds = new PrometheusDatasource(instanceSettings, $q, backendSrv, templateSrv, timeSrv); - await ctx.ds.query(query); - let res = backendSrv.datasourceRequest.mock.calls[0][0]; - expect(res.method).toBe('GET'); - expect(res.url).toBe(urlExpected); - - expect(templateSrv.replace.mock.calls[0][1]).toEqual({ - __interval: { - text: '10s', - value: '10s', - }, - __interval_ms: { - text: 10000, - value: 10000, - }, - }); - - expect(query.scopedVars.__interval.text).toBe('10s'); - expect(query.scopedVars.__interval.value).toBe('10s'); - expect(query.scopedVars.__interval_ms.text).toBe(10 * 1000); - expect(query.scopedVars.__interval_ms.value).toBe(10 * 1000); - }); - it('should be interval * intervalFactor when greater than min interval', async () => { - var query = { - // 6 minute range - range: { from: time({ minutes: 1 }), to: time({ minutes: 7 }) }, - targets: [ - { - expr: 'rate(test[$__interval])', - interval: '10s', - intervalFactor: 10, - }, - ], - interval: '5s', - scopedVars: { - __interval: { text: '5s', value: '5s' }, - __interval_ms: { text: 5 * 1000, value: 5 * 1000 }, - }, - }; - var urlExpected = - 'proxied/api/v1/query_range?query=' + - encodeURIComponent('rate(test[$__interval])') + - '&start=50&end=450&step=50'; - - templateSrv.replace = jest.fn(str => str); - backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response)); - ctx.ds = new PrometheusDatasource(instanceSettings, $q, backendSrv, templateSrv, timeSrv); - await ctx.ds.query(query); - let res = backendSrv.datasourceRequest.mock.calls[0][0]; - expect(res.method).toBe('GET'); - expect(res.url).toBe(urlExpected); - - expect(templateSrv.replace.mock.calls[0][1]).toEqual({ - __interval: { - text: '5s', - value: '5s', - }, - __interval_ms: { - text: 5000, - value: 5000, - }, - }); - }); - it('should be min interval when greater than interval * intervalFactor', async () => { - var query = { - // 6 minute range - range: { from: time({ minutes: 1 }), to: time({ minutes: 7 }) }, - targets: [ - { - expr: 'rate(test[$__interval])', - interval: '15s', - intervalFactor: 2, - }, - ], - interval: '5s', - scopedVars: { - __interval: { text: '5s', value: '5s' }, - __interval_ms: { text: 5 * 1000, value: 5 * 1000 }, - }, - }; - var urlExpected = - 'proxied/api/v1/query_range?query=' + - encodeURIComponent('rate(test[$__interval])') + - '&start=60&end=420&step=15'; - - backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response)); - ctx.ds = new PrometheusDatasource(instanceSettings, $q, backendSrv, templateSrv, timeSrv); - await ctx.ds.query(query); - let res = backendSrv.datasourceRequest.mock.calls[0][0]; - expect(res.method).toBe('GET'); - expect(res.url).toBe(urlExpected); - - expect(templateSrv.replace.mock.calls[0][1]).toEqual({ - __interval: { - text: '5s', - value: '5s', - }, - __interval_ms: { - text: 5000, - value: 5000, - }, - }); - }); - it('should be determined by the 11000 data points limit, accounting for intervalFactor', async () => { - var query = { - // 1 week range - range: { from: time({}), to: time({ hours: 7 * 24 }) }, - targets: [ - { - expr: 'rate(test[$__interval])', - intervalFactor: 10, - }, - ], - interval: '5s', - scopedVars: { - __interval: { text: '5s', value: '5s' }, - __interval_ms: { text: 5 * 1000, value: 5 * 1000 }, - }, - }; - var end = 7 * 24 * 60 * 60; - var start = 0; - var urlExpected = - 'proxied/api/v1/query_range?query=' + - encodeURIComponent('rate(test[$__interval])') + - '&start=' + - start + - '&end=' + - end + - '&step=60'; - backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response)); - templateSrv.replace = jest.fn(str => str); - ctx.ds = new PrometheusDatasource(instanceSettings, $q, backendSrv, templateSrv, timeSrv); - await ctx.ds.query(query); - let res = backendSrv.datasourceRequest.mock.calls[0][0]; - expect(res.method).toBe('GET'); - expect(res.url).toBe(urlExpected); - - expect(templateSrv.replace.mock.calls[0][1]).toEqual({ - __interval: { - text: '5s', - value: '5s', - }, - __interval_ms: { - text: 5000, - value: 5000, - }, - }); - }); - }); -}); - -describe('PrometheusDatasource for POST', function() { - // var ctx = new helpers.ServiceTestContext(); - let instanceSettings = { - url: 'proxied', - directUrl: 'direct', - user: 'test', - password: 'mupp', - jsonData: { httpMethod: 'POST' }, - }; - - // beforeEach(angularMocks.module('grafana.core')); - // beforeEach(angularMocks.module('grafana.services')); - // beforeEach(ctx.providePhase(['timeSrv'])); - - // beforeEach( - // // angularMocks.inject(function($q, $rootScope, $httpBackend, $injector) { - // // ctx.$q = $q; - // // ctx.$httpBackend = $httpBackend; - // // ctx.$rootScope = $rootScope; - // // ctx.ds = $injector.instantiate(PrometheusDatasource, { instanceSettings: instanceSettings }); - // // $httpBackend.when('GET', /\.html$/).respond(''); - // // }) - // ); - - describe('When querying prometheus with one target using query editor target spec', function() { - var results; - var urlExpected = 'proxied/api/v1/query_range'; - var dataExpected = { - query: 'test{job="testjob"}', - start: 1 * 60, - end: 3 * 60, - step: 60, - }; - var query = { - range: { from: time({ minutes: 1, seconds: 3 }), to: time({ minutes: 2, seconds: 3 }) }, - targets: [{ expr: 'test{job="testjob"}', format: 'time_series' }], - interval: '60s', - }; - - beforeEach(async () => { - let response = { - status: 'success', - data: { - data: { - resultType: 'matrix', - result: [ - { - metric: { __name__: 'test', job: 'testjob' }, - values: [[2 * 60, '3846']], - }, - ], - }, - }, - }; - backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response)); - ctx.ds = new PrometheusDatasource(instanceSettings, $q, backendSrv, templateSrv, timeSrv); - await ctx.ds.query(query).then(function(data) { - results = data; - }); - }); - it('should generate the correct query', function() { - let res = backendSrv.datasourceRequest.mock.calls[0][0]; - expect(res.method).toBe('POST'); - expect(res.url).toBe(urlExpected); - expect(res.data).toEqual(dataExpected); - }); - it('should return series list', function() { - expect(results.data.length).toBe(1); - expect(results.data[0].target).toBe('test{job="testjob"}'); - }); - }); -}); diff --git a/public/app/plugins/datasource/prometheus/specs/datasource.jest.ts b/public/app/plugins/datasource/prometheus/specs/datasource.jest.ts index b8b2b50f590..f60af583f45 100644 --- a/public/app/plugins/datasource/prometheus/specs/datasource.jest.ts +++ b/public/app/plugins/datasource/prometheus/specs/datasource.jest.ts @@ -246,3 +246,797 @@ describe('PrometheusDatasource', () => { }); }); }); + +const SECOND = 1000; +const MINUTE = 60 * SECOND; +const HOUR = 60 * MINUTE; + +const time = ({ hours = 0, seconds = 0, minutes = 0 }) => moment(hours * HOUR + minutes * MINUTE + seconds * SECOND); + +let ctx = {}; +let instanceSettings = { + url: 'proxied', + directUrl: 'direct', + user: 'test', + password: 'mupp', + jsonData: { httpMethod: 'GET' }, +}; +let backendSrv = { + datasourceRequest: jest.fn(), +}; + +let templateSrv = { + replace: jest.fn(str => str), +}; + +let timeSrv = { + timeRange: () => { + return { to: { diff: () => 2000 }, from: '' }; + }, +}; + +describe('PrometheusDatasource', function() { + describe('When querying prometheus with one target using query editor target spec', async () => { + var results; + var query = { + range: { from: time({ seconds: 63 }), to: time({ seconds: 183 }) }, + targets: [{ expr: 'test{job="testjob"}', format: 'time_series' }], + interval: '60s', + }; + // Interval alignment with step + var urlExpected = + 'proxied/api/v1/query_range?query=' + encodeURIComponent('test{job="testjob"}') + '&start=60&end=240&step=60'; + + beforeEach(async () => { + let response = { + data: { + status: 'success', + data: { + resultType: 'matrix', + result: [ + { + metric: { __name__: 'test', job: 'testjob' }, + values: [[60, '3846']], + }, + ], + }, + }, + }; + backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response)); + ctx.ds = new PrometheusDatasource(instanceSettings, q, backendSrv, templateSrv, timeSrv); + + await ctx.ds.query(query).then(function(data) { + results = data; + }); + }); + + it('should generate the correct query', function() { + let res = backendSrv.datasourceRequest.mock.calls[0][0]; + expect(res.method).toBe('GET'); + expect(res.url).toBe(urlExpected); + }); + it('should return series list', async () => { + expect(results.data.length).toBe(1); + expect(results.data[0].target).toBe('test{job="testjob"}'); + }); + }); + describe('When querying prometheus with one target which return multiple series', function() { + var results; + var start = 60; + var end = 360; + var step = 60; + + var query = { + range: { from: time({ seconds: start }), to: time({ seconds: end }) }, + targets: [{ expr: 'test{job="testjob"}', format: 'time_series' }], + interval: '60s', + }; + + beforeEach(async () => { + let response = { + status: 'success', + data: { + data: { + resultType: 'matrix', + result: [ + { + metric: { __name__: 'test', job: 'testjob', series: 'series 1' }, + values: [[start + step * 1, '3846'], [start + step * 3, '3847'], [end - step * 1, '3848']], + }, + { + metric: { __name__: 'test', job: 'testjob', series: 'series 2' }, + values: [[start + step * 2, '4846']], + }, + ], + }, + }, + }; + + backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response)); + ctx.ds = new PrometheusDatasource(instanceSettings, q, backendSrv, templateSrv, timeSrv); + + await ctx.ds.query(query).then(function(data) { + results = data; + }); + }); + + it('should be same length', function() { + expect(results.data.length).toBe(2); + expect(results.data[0].datapoints.length).toBe((end - start) / step + 1); + expect(results.data[1].datapoints.length).toBe((end - start) / step + 1); + }); + + it('should fill null until first datapoint in response', function() { + expect(results.data[0].datapoints[0][1]).toBe(start * 1000); + expect(results.data[0].datapoints[0][0]).toBe(null); + expect(results.data[0].datapoints[1][1]).toBe((start + step * 1) * 1000); + expect(results.data[0].datapoints[1][0]).toBe(3846); + }); + it('should fill null after last datapoint in response', function() { + var length = (end - start) / step + 1; + expect(results.data[0].datapoints[length - 2][1]).toBe((end - step * 1) * 1000); + expect(results.data[0].datapoints[length - 2][0]).toBe(3848); + expect(results.data[0].datapoints[length - 1][1]).toBe(end * 1000); + expect(results.data[0].datapoints[length - 1][0]).toBe(null); + }); + it('should fill null at gap between series', function() { + expect(results.data[0].datapoints[2][1]).toBe((start + step * 2) * 1000); + expect(results.data[0].datapoints[2][0]).toBe(null); + expect(results.data[1].datapoints[1][1]).toBe((start + step * 1) * 1000); + expect(results.data[1].datapoints[1][0]).toBe(null); + expect(results.data[1].datapoints[3][1]).toBe((start + step * 3) * 1000); + expect(results.data[1].datapoints[3][0]).toBe(null); + }); + }); + describe('When querying prometheus with one target and instant = true', function() { + var results; + var urlExpected = 'proxied/api/v1/query?query=' + encodeURIComponent('test{job="testjob"}') + '&time=123'; + var query = { + range: { from: time({ seconds: 63 }), to: time({ seconds: 123 }) }, + targets: [{ expr: 'test{job="testjob"}', format: 'time_series', instant: true }], + interval: '60s', + }; + + beforeEach(async () => { + let response = { + status: 'success', + data: { + data: { + resultType: 'vector', + result: [ + { + metric: { __name__: 'test', job: 'testjob' }, + value: [123, '3846'], + }, + ], + }, + }, + }; + + backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response)); + ctx.ds = new PrometheusDatasource(instanceSettings, q, backendSrv, templateSrv, timeSrv); + + await ctx.ds.query(query).then(function(data) { + results = data; + }); + }); + it('should generate the correct query', function() { + let res = backendSrv.datasourceRequest.mock.calls[0][0]; + expect(res.method).toBe('GET'); + expect(res.url).toBe(urlExpected); + }); + it('should return series list', function() { + expect(results.data.length).toBe(1); + expect(results.data[0].target).toBe('test{job="testjob"}'); + }); + }); + describe('When performing annotationQuery', function() { + var results; + + var options = { + annotation: { + expr: 'ALERTS{alertstate="firing"}', + tagKeys: 'job', + titleFormat: '{{alertname}}', + textFormat: '{{instance}}', + }, + range: { + from: time({ seconds: 63 }), + to: time({ seconds: 123 }), + }, + }; + + beforeEach(async () => { + let response = { + status: 'success', + data: { + data: { + resultType: 'matrix', + result: [ + { + metric: { + __name__: 'ALERTS', + alertname: 'InstanceDown', + alertstate: 'firing', + instance: 'testinstance', + job: 'testjob', + }, + values: [[123, '1']], + }, + ], + }, + }, + }; + + backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response)); + ctx.ds = new PrometheusDatasource(instanceSettings, q, backendSrv, templateSrv, timeSrv); + + await ctx.ds.annotationQuery(options).then(function(data) { + results = data; + }); + }); + it('should return annotation list', function() { + expect(results.length).toBe(1); + expect(results[0].tags).toContain('testjob'); + expect(results[0].title).toBe('InstanceDown'); + expect(results[0].text).toBe('testinstance'); + expect(results[0].time).toBe(123 * 1000); + }); + }); + + describe('When resultFormat is table and instant = true', function() { + var results; + var query = { + range: { from: time({ seconds: 63 }), to: time({ seconds: 123 }) }, + targets: [{ expr: 'test{job="testjob"}', format: 'time_series', instant: true }], + interval: '60s', + }; + + beforeEach(async () => { + let response = { + status: 'success', + data: { + data: { + resultType: 'vector', + result: [ + { + metric: { __name__: 'test', job: 'testjob' }, + value: [123, '3846'], + }, + ], + }, + }, + }; + + backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response)); + ctx.ds = new PrometheusDatasource(instanceSettings, q, backendSrv, templateSrv, timeSrv); + await ctx.ds.query(query).then(function(data) { + results = data; + }); + }); + + it('should return result', () => { + expect(results).not.toBe(null); + }); + }); + + describe('The "step" query parameter', function() { + var response = { + status: 'success', + data: { + data: { + resultType: 'matrix', + result: [], + }, + }, + }; + + it('should be min interval when greater than auto interval', async () => { + let query = { + // 6 minute range + range: { from: time({ minutes: 1 }), to: time({ minutes: 7 }) }, + targets: [ + { + expr: 'test', + interval: '10s', + }, + ], + interval: '5s', + }; + let urlExpected = 'proxied/api/v1/query_range?query=test&start=60&end=420&step=10'; + + backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response)); + ctx.ds = new PrometheusDatasource(instanceSettings, q, backendSrv, templateSrv, timeSrv); + await ctx.ds.query(query); + let res = backendSrv.datasourceRequest.mock.calls[0][0]; + expect(res.method).toBe('GET'); + expect(res.url).toBe(urlExpected); + }); + + it('step should never go below 1', async () => { + var query = { + // 6 minute range + range: { from: time({ minutes: 1 }), to: time({ minutes: 7 }) }, + targets: [{ expr: 'test' }], + interval: '100ms', + }; + var urlExpected = 'proxied/api/v1/query_range?query=test&start=60&end=420&step=1'; + backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response)); + ctx.ds = new PrometheusDatasource(instanceSettings, q, backendSrv, templateSrv, timeSrv); + await ctx.ds.query(query); + let res = backendSrv.datasourceRequest.mock.calls[0][0]; + expect(res.method).toBe('GET'); + expect(res.url).toBe(urlExpected); + }); + + it('should be auto interval when greater than min interval', async () => { + var query = { + // 6 minute range + range: { from: time({ minutes: 1 }), to: time({ minutes: 7 }) }, + targets: [ + { + expr: 'test', + interval: '5s', + }, + ], + interval: '10s', + }; + var urlExpected = 'proxied/api/v1/query_range?query=test&start=60&end=420&step=10'; + backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response)); + ctx.ds = new PrometheusDatasource(instanceSettings, q, backendSrv, templateSrv, timeSrv); + await ctx.ds.query(query); + let res = backendSrv.datasourceRequest.mock.calls[0][0]; + expect(res.method).toBe('GET'); + expect(res.url).toBe(urlExpected); + }); + it('should result in querying fewer than 11000 data points', async () => { + var query = { + // 6 hour range + range: { from: time({ hours: 1 }), to: time({ hours: 7 }) }, + targets: [{ expr: 'test' }], + interval: '1s', + }; + var end = 7 * 60 * 60; + var start = 60 * 60; + var urlExpected = 'proxied/api/v1/query_range?query=test&start=' + start + '&end=' + end + '&step=2'; + backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response)); + ctx.ds = new PrometheusDatasource(instanceSettings, q, backendSrv, templateSrv, timeSrv); + await ctx.ds.query(query); + let res = backendSrv.datasourceRequest.mock.calls[0][0]; + expect(res.method).toBe('GET'); + expect(res.url).toBe(urlExpected); + }); + it('should not apply min interval when interval * intervalFactor greater', async () => { + var query = { + // 6 minute range + range: { from: time({ minutes: 1 }), to: time({ minutes: 7 }) }, + targets: [ + { + expr: 'test', + interval: '10s', + intervalFactor: 10, + }, + ], + interval: '5s', + }; + // times get rounded up to interval + var urlExpected = 'proxied/api/v1/query_range?query=test&start=50&end=450&step=50'; + backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response)); + ctx.ds = new PrometheusDatasource(instanceSettings, q, backendSrv, templateSrv, timeSrv); + await ctx.ds.query(query); + let res = backendSrv.datasourceRequest.mock.calls[0][0]; + expect(res.method).toBe('GET'); + expect(res.url).toBe(urlExpected); + }); + it('should apply min interval when interval * intervalFactor smaller', async () => { + var query = { + // 6 minute range + range: { from: time({ minutes: 1 }), to: time({ minutes: 7 }) }, + targets: [ + { + expr: 'test', + interval: '15s', + intervalFactor: 2, + }, + ], + interval: '5s', + }; + var urlExpected = 'proxied/api/v1/query_range?query=test' + '&start=60&end=420&step=15'; + backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response)); + ctx.ds = new PrometheusDatasource(instanceSettings, q, backendSrv, templateSrv, timeSrv); + await ctx.ds.query(query); + let res = backendSrv.datasourceRequest.mock.calls[0][0]; + expect(res.method).toBe('GET'); + expect(res.url).toBe(urlExpected); + }); + it('should apply intervalFactor to auto interval when greater', async () => { + var query = { + // 6 minute range + range: { from: time({ minutes: 1 }), to: time({ minutes: 7 }) }, + targets: [ + { + expr: 'test', + interval: '5s', + intervalFactor: 10, + }, + ], + interval: '10s', + }; + // times get aligned to interval + var urlExpected = 'proxied/api/v1/query_range?query=test' + '&start=0&end=500&step=100'; + backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response)); + ctx.ds = new PrometheusDatasource(instanceSettings, q, backendSrv, templateSrv, timeSrv); + await ctx.ds.query(query); + let res = backendSrv.datasourceRequest.mock.calls[0][0]; + expect(res.method).toBe('GET'); + expect(res.url).toBe(urlExpected); + }); + it('should not not be affected by the 11000 data points limit when large enough', async () => { + var query = { + // 1 week range + range: { from: time({}), to: time({ hours: 7 * 24 }) }, + targets: [ + { + expr: 'test', + intervalFactor: 10, + }, + ], + interval: '10s', + }; + var end = 7 * 24 * 60 * 60; + var start = 0; + var urlExpected = 'proxied/api/v1/query_range?query=test' + '&start=' + start + '&end=' + end + '&step=100'; + backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response)); + ctx.ds = new PrometheusDatasource(instanceSettings, q, backendSrv, templateSrv, timeSrv); + await ctx.ds.query(query); + let res = backendSrv.datasourceRequest.mock.calls[0][0]; + expect(res.method).toBe('GET'); + expect(res.url).toBe(urlExpected); + }); + it('should be determined by the 11000 data points limit when too small', async () => { + var query = { + // 1 week range + range: { from: time({}), to: time({ hours: 7 * 24 }) }, + targets: [ + { + expr: 'test', + intervalFactor: 10, + }, + ], + interval: '5s', + }; + var end = 7 * 24 * 60 * 60; + var start = 0; + var urlExpected = 'proxied/api/v1/query_range?query=test' + '&start=' + start + '&end=' + end + '&step=60'; + backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response)); + ctx.ds = new PrometheusDatasource(instanceSettings, q, backendSrv, templateSrv, timeSrv); + await ctx.ds.query(query); + let res = backendSrv.datasourceRequest.mock.calls[0][0]; + expect(res.method).toBe('GET'); + expect(res.url).toBe(urlExpected); + }); + }); + + describe('The __interval and __interval_ms template variables', function() { + var response = { + status: 'success', + data: { + data: { + resultType: 'matrix', + result: [], + }, + }, + }; + + it('should be unchanged when auto interval is greater than min interval', async () => { + var query = { + // 6 minute range + range: { from: time({ minutes: 1 }), to: time({ minutes: 7 }) }, + targets: [ + { + expr: 'rate(test[$__interval])', + interval: '5s', + }, + ], + interval: '10s', + scopedVars: { + __interval: { text: '10s', value: '10s' }, + __interval_ms: { text: 10 * 1000, value: 10 * 1000 }, + }, + }; + + var urlExpected = + 'proxied/api/v1/query_range?query=' + + encodeURIComponent('rate(test[$__interval])') + + '&start=60&end=420&step=10'; + + templateSrv.replace = jest.fn(str => str); + backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response)); + ctx.ds = new PrometheusDatasource(instanceSettings, q, backendSrv, templateSrv, timeSrv); + await ctx.ds.query(query); + let res = backendSrv.datasourceRequest.mock.calls[0][0]; + expect(res.method).toBe('GET'); + expect(res.url).toBe(urlExpected); + + expect(templateSrv.replace.mock.calls[0][1]).toEqual({ + __interval: { + text: '10s', + value: '10s', + }, + __interval_ms: { + text: 10000, + value: 10000, + }, + }); + }); + it('should be min interval when it is greater than auto interval', async () => { + var query = { + // 6 minute range + range: { from: time({ minutes: 1 }), to: time({ minutes: 7 }) }, + targets: [ + { + expr: 'rate(test[$__interval])', + interval: '10s', + }, + ], + interval: '5s', + scopedVars: { + __interval: { text: '5s', value: '5s' }, + __interval_ms: { text: 5 * 1000, value: 5 * 1000 }, + }, + }; + var urlExpected = + 'proxied/api/v1/query_range?query=' + + encodeURIComponent('rate(test[$__interval])') + + '&start=60&end=420&step=10'; + backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response)); + templateSrv.replace = jest.fn(str => str); + ctx.ds = new PrometheusDatasource(instanceSettings, q, backendSrv, templateSrv, timeSrv); + await ctx.ds.query(query); + let res = backendSrv.datasourceRequest.mock.calls[0][0]; + expect(res.method).toBe('GET'); + expect(res.url).toBe(urlExpected); + + expect(templateSrv.replace.mock.calls[0][1]).toEqual({ + __interval: { + text: '5s', + value: '5s', + }, + __interval_ms: { + text: 5000, + value: 5000, + }, + }); + }); + it('should account for intervalFactor', async () => { + var query = { + // 6 minute range + range: { from: time({ minutes: 1 }), to: time({ minutes: 7 }) }, + targets: [ + { + expr: 'rate(test[$__interval])', + interval: '5s', + intervalFactor: 10, + }, + ], + interval: '10s', + scopedVars: { + __interval: { text: '10s', value: '10s' }, + __interval_ms: { text: 10 * 1000, value: 10 * 1000 }, + }, + }; + var urlExpected = + 'proxied/api/v1/query_range?query=' + + encodeURIComponent('rate(test[$__interval])') + + '&start=0&end=500&step=100'; + backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response)); + templateSrv.replace = jest.fn(str => str); + ctx.ds = new PrometheusDatasource(instanceSettings, q, backendSrv, templateSrv, timeSrv); + await ctx.ds.query(query); + let res = backendSrv.datasourceRequest.mock.calls[0][0]; + expect(res.method).toBe('GET'); + expect(res.url).toBe(urlExpected); + + expect(templateSrv.replace.mock.calls[0][1]).toEqual({ + __interval: { + text: '10s', + value: '10s', + }, + __interval_ms: { + text: 10000, + value: 10000, + }, + }); + + expect(query.scopedVars.__interval.text).toBe('10s'); + expect(query.scopedVars.__interval.value).toBe('10s'); + expect(query.scopedVars.__interval_ms.text).toBe(10 * 1000); + expect(query.scopedVars.__interval_ms.value).toBe(10 * 1000); + }); + it('should be interval * intervalFactor when greater than min interval', async () => { + var query = { + // 6 minute range + range: { from: time({ minutes: 1 }), to: time({ minutes: 7 }) }, + targets: [ + { + expr: 'rate(test[$__interval])', + interval: '10s', + intervalFactor: 10, + }, + ], + interval: '5s', + scopedVars: { + __interval: { text: '5s', value: '5s' }, + __interval_ms: { text: 5 * 1000, value: 5 * 1000 }, + }, + }; + var urlExpected = + 'proxied/api/v1/query_range?query=' + + encodeURIComponent('rate(test[$__interval])') + + '&start=50&end=450&step=50'; + + templateSrv.replace = jest.fn(str => str); + backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response)); + ctx.ds = new PrometheusDatasource(instanceSettings, q, backendSrv, templateSrv, timeSrv); + await ctx.ds.query(query); + let res = backendSrv.datasourceRequest.mock.calls[0][0]; + expect(res.method).toBe('GET'); + expect(res.url).toBe(urlExpected); + + expect(templateSrv.replace.mock.calls[0][1]).toEqual({ + __interval: { + text: '5s', + value: '5s', + }, + __interval_ms: { + text: 5000, + value: 5000, + }, + }); + }); + it('should be min interval when greater than interval * intervalFactor', async () => { + var query = { + // 6 minute range + range: { from: time({ minutes: 1 }), to: time({ minutes: 7 }) }, + targets: [ + { + expr: 'rate(test[$__interval])', + interval: '15s', + intervalFactor: 2, + }, + ], + interval: '5s', + scopedVars: { + __interval: { text: '5s', value: '5s' }, + __interval_ms: { text: 5 * 1000, value: 5 * 1000 }, + }, + }; + var urlExpected = + 'proxied/api/v1/query_range?query=' + + encodeURIComponent('rate(test[$__interval])') + + '&start=60&end=420&step=15'; + + backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response)); + ctx.ds = new PrometheusDatasource(instanceSettings, q, backendSrv, templateSrv, timeSrv); + await ctx.ds.query(query); + let res = backendSrv.datasourceRequest.mock.calls[0][0]; + expect(res.method).toBe('GET'); + expect(res.url).toBe(urlExpected); + + expect(templateSrv.replace.mock.calls[0][1]).toEqual({ + __interval: { + text: '5s', + value: '5s', + }, + __interval_ms: { + text: 5000, + value: 5000, + }, + }); + }); + it('should be determined by the 11000 data points limit, accounting for intervalFactor', async () => { + var query = { + // 1 week range + range: { from: time({}), to: time({ hours: 7 * 24 }) }, + targets: [ + { + expr: 'rate(test[$__interval])', + intervalFactor: 10, + }, + ], + interval: '5s', + scopedVars: { + __interval: { text: '5s', value: '5s' }, + __interval_ms: { text: 5 * 1000, value: 5 * 1000 }, + }, + }; + var end = 7 * 24 * 60 * 60; + var start = 0; + var urlExpected = + 'proxied/api/v1/query_range?query=' + + encodeURIComponent('rate(test[$__interval])') + + '&start=' + + start + + '&end=' + + end + + '&step=60'; + backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response)); + templateSrv.replace = jest.fn(str => str); + ctx.ds = new PrometheusDatasource(instanceSettings, q, backendSrv, templateSrv, timeSrv); + await ctx.ds.query(query); + let res = backendSrv.datasourceRequest.mock.calls[0][0]; + expect(res.method).toBe('GET'); + expect(res.url).toBe(urlExpected); + + expect(templateSrv.replace.mock.calls[0][1]).toEqual({ + __interval: { + text: '5s', + value: '5s', + }, + __interval_ms: { + text: 5000, + value: 5000, + }, + }); + }); + }); +}); + +describe('PrometheusDatasource for POST', function() { + // var ctx = new helpers.ServiceTestContext(); + let instanceSettings = { + url: 'proxied', + directUrl: 'direct', + user: 'test', + password: 'mupp', + jsonData: { httpMethod: 'POST' }, + }; + + describe('When querying prometheus with one target using query editor target spec', function() { + var results; + var urlExpected = 'proxied/api/v1/query_range'; + var dataExpected = { + query: 'test{job="testjob"}', + start: 1 * 60, + end: 3 * 60, + step: 60, + }; + var query = { + range: { from: time({ minutes: 1, seconds: 3 }), to: time({ minutes: 2, seconds: 3 }) }, + targets: [{ expr: 'test{job="testjob"}', format: 'time_series' }], + interval: '60s', + }; + + beforeEach(async () => { + let response = { + status: 'success', + data: { + data: { + resultType: 'matrix', + result: [ + { + metric: { __name__: 'test', job: 'testjob' }, + values: [[2 * 60, '3846']], + }, + ], + }, + }, + }; + backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response)); + ctx.ds = new PrometheusDatasource(instanceSettings, q, backendSrv, templateSrv, timeSrv); + await ctx.ds.query(query).then(function(data) { + results = data; + }); + }); + it('should generate the correct query', function() { + let res = backendSrv.datasourceRequest.mock.calls[0][0]; + expect(res.method).toBe('POST'); + expect(res.url).toBe(urlExpected); + expect(res.data).toEqual(dataExpected); + }); + it('should return series list', function() { + expect(results.data.length).toBe(1); + expect(results.data[0].target).toBe('test{job="testjob"}'); + }); + }); +}); From 951b623bd23ca1aa43833e2898876579c8417370 Mon Sep 17 00:00:00 2001 From: Tobias Skarhed Date: Wed, 1 Aug 2018 14:27:45 +0200 Subject: [PATCH 241/786] Change to arrow functions --- .../prometheus/specs/datasource.jest.ts | 66 +++++++++---------- 1 file changed, 33 insertions(+), 33 deletions(-) diff --git a/public/app/plugins/datasource/prometheus/specs/datasource.jest.ts b/public/app/plugins/datasource/prometheus/specs/datasource.jest.ts index f60af583f45..aeca8d69191 100644 --- a/public/app/plugins/datasource/prometheus/specs/datasource.jest.ts +++ b/public/app/plugins/datasource/prometheus/specs/datasource.jest.ts @@ -150,49 +150,49 @@ describe('PrometheusDatasource', () => { }); }); - describe('alignRange', function() { - it('does not modify already aligned intervals with perfect step', function() { + describe('alignRange', () => { + it('does not modify already aligned intervals with perfect step', () => { const range = alignRange(0, 3, 3); expect(range.start).toEqual(0); expect(range.end).toEqual(3); }); - it('does modify end-aligned intervals to reflect number of steps possible', function() { + it('does modify end-aligned intervals to reflect number of steps possible', () => { const range = alignRange(1, 6, 3); expect(range.start).toEqual(0); expect(range.end).toEqual(6); }); - it('does align intervals that are a multiple of steps', function() { + it('does align intervals that are a multiple of steps', () => { const range = alignRange(1, 4, 3); expect(range.start).toEqual(0); expect(range.end).toEqual(6); }); - it('does align intervals that are not a multiple of steps', function() { + it('does align intervals that are not a multiple of steps', () => { const range = alignRange(1, 5, 3); expect(range.start).toEqual(0); expect(range.end).toEqual(6); }); }); - describe('Prometheus regular escaping', function() { - it('should not escape non-string', function() { + describe('Prometheus regular escaping', () => { + it('should not escape non-string', () => { expect(prometheusRegularEscape(12)).toEqual(12); }); - it('should not escape simple string', function() { + it('should not escape simple string', () => { expect(prometheusRegularEscape('cryptodepression')).toEqual('cryptodepression'); }); - it("should escape '", function() { + it("should escape '", () => { expect(prometheusRegularEscape("looking'glass")).toEqual("looking\\\\'glass"); }); - it('should escape multiple characters', function() { + it('should escape multiple characters', () => { expect(prometheusRegularEscape("'looking'glass'")).toEqual("\\\\'looking\\\\'glass\\\\'"); }); }); - describe('Prometheus regexes escaping', function() { - it('should not escape simple string', function() { + describe('Prometheus regexes escaping', () => { + it('should not escape simple string', () => { expect(prometheusSpecialRegexEscape('cryptodepression')).toEqual('cryptodepression'); }); - it('should escape $^*+?.()\\', function() { + it('should escape $^*+?.()\\', () => { expect(prometheusSpecialRegexEscape("looking'glass")).toEqual("looking\\\\'glass"); expect(prometheusSpecialRegexEscape('looking{glass')).toEqual('looking\\\\{glass'); expect(prometheusSpecialRegexEscape('looking}glass')).toEqual('looking\\\\}glass'); @@ -208,7 +208,7 @@ describe('PrometheusDatasource', () => { expect(prometheusSpecialRegexEscape('looking)glass')).toEqual('looking\\\\)glass'); expect(prometheusSpecialRegexEscape('looking\\glass')).toEqual('looking\\\\\\\\glass'); }); - it('should escape multiple special characters', function() { + it('should escape multiple special characters', () => { expect(prometheusSpecialRegexEscape('+looking$glass?')).toEqual('\\\\+looking\\\\$glass\\\\?'); }); }); @@ -275,7 +275,7 @@ let timeSrv = { }, }; -describe('PrometheusDatasource', function() { +describe('PrometheusDatasource', () => { describe('When querying prometheus with one target using query editor target spec', async () => { var results; var query = { @@ -310,7 +310,7 @@ describe('PrometheusDatasource', function() { }); }); - it('should generate the correct query', function() { + it('should generate the correct query', () => { let res = backendSrv.datasourceRequest.mock.calls[0][0]; expect(res.method).toBe('GET'); expect(res.url).toBe(urlExpected); @@ -320,7 +320,7 @@ describe('PrometheusDatasource', function() { expect(results.data[0].target).toBe('test{job="testjob"}'); }); }); - describe('When querying prometheus with one target which return multiple series', function() { + describe('When querying prometheus with one target which return multiple series', () => { var results; var start = 60; var end = 360; @@ -360,26 +360,26 @@ describe('PrometheusDatasource', function() { }); }); - it('should be same length', function() { + it('should be same length', () => { expect(results.data.length).toBe(2); expect(results.data[0].datapoints.length).toBe((end - start) / step + 1); expect(results.data[1].datapoints.length).toBe((end - start) / step + 1); }); - it('should fill null until first datapoint in response', function() { + it('should fill null until first datapoint in response', () => { expect(results.data[0].datapoints[0][1]).toBe(start * 1000); expect(results.data[0].datapoints[0][0]).toBe(null); expect(results.data[0].datapoints[1][1]).toBe((start + step * 1) * 1000); expect(results.data[0].datapoints[1][0]).toBe(3846); }); - it('should fill null after last datapoint in response', function() { + it('should fill null after last datapoint in response', () => { var length = (end - start) / step + 1; expect(results.data[0].datapoints[length - 2][1]).toBe((end - step * 1) * 1000); expect(results.data[0].datapoints[length - 2][0]).toBe(3848); expect(results.data[0].datapoints[length - 1][1]).toBe(end * 1000); expect(results.data[0].datapoints[length - 1][0]).toBe(null); }); - it('should fill null at gap between series', function() { + it('should fill null at gap between series', () => { expect(results.data[0].datapoints[2][1]).toBe((start + step * 2) * 1000); expect(results.data[0].datapoints[2][0]).toBe(null); expect(results.data[1].datapoints[1][1]).toBe((start + step * 1) * 1000); @@ -388,7 +388,7 @@ describe('PrometheusDatasource', function() { expect(results.data[1].datapoints[3][0]).toBe(null); }); }); - describe('When querying prometheus with one target and instant = true', function() { + describe('When querying prometheus with one target and instant = true', () => { var results; var urlExpected = 'proxied/api/v1/query?query=' + encodeURIComponent('test{job="testjob"}') + '&time=123'; var query = { @@ -420,17 +420,17 @@ describe('PrometheusDatasource', function() { results = data; }); }); - it('should generate the correct query', function() { + it('should generate the correct query', () => { let res = backendSrv.datasourceRequest.mock.calls[0][0]; expect(res.method).toBe('GET'); expect(res.url).toBe(urlExpected); }); - it('should return series list', function() { + it('should return series list', () => { expect(results.data.length).toBe(1); expect(results.data[0].target).toBe('test{job="testjob"}'); }); }); - describe('When performing annotationQuery', function() { + describe('When performing annotationQuery', () => { var results; var options = { @@ -475,7 +475,7 @@ describe('PrometheusDatasource', function() { results = data; }); }); - it('should return annotation list', function() { + it('should return annotation list', () => { expect(results.length).toBe(1); expect(results[0].tags).toContain('testjob'); expect(results[0].title).toBe('InstanceDown'); @@ -484,7 +484,7 @@ describe('PrometheusDatasource', function() { }); }); - describe('When resultFormat is table and instant = true', function() { + describe('When resultFormat is table and instant = true', () => { var results; var query = { range: { from: time({ seconds: 63 }), to: time({ seconds: 123 }) }, @@ -520,7 +520,7 @@ describe('PrometheusDatasource', function() { }); }); - describe('The "step" query parameter', function() { + describe('The "step" query parameter', () => { var response = { status: 'success', data: { @@ -717,7 +717,7 @@ describe('PrometheusDatasource', function() { }); }); - describe('The __interval and __interval_ms template variables', function() { + describe('The __interval and __interval_ms template variables', () => { var response = { status: 'success', data: { @@ -982,7 +982,7 @@ describe('PrometheusDatasource', function() { }); }); -describe('PrometheusDatasource for POST', function() { +describe('PrometheusDatasource for POST', () => { // var ctx = new helpers.ServiceTestContext(); let instanceSettings = { url: 'proxied', @@ -992,7 +992,7 @@ describe('PrometheusDatasource for POST', function() { jsonData: { httpMethod: 'POST' }, }; - describe('When querying prometheus with one target using query editor target spec', function() { + describe('When querying prometheus with one target using query editor target spec', () => { var results; var urlExpected = 'proxied/api/v1/query_range'; var dataExpected = { @@ -1028,13 +1028,13 @@ describe('PrometheusDatasource for POST', function() { results = data; }); }); - it('should generate the correct query', function() { + it('should generate the correct query', () => { let res = backendSrv.datasourceRequest.mock.calls[0][0]; expect(res.method).toBe('POST'); expect(res.url).toBe(urlExpected); expect(res.data).toEqual(dataExpected); }); - it('should return series list', function() { + it('should return series list', () => { expect(results.data.length).toBe(1); expect(results.data[0].target).toBe('test{job="testjob"}'); }); From dc22e24642f79b1130de2fc4f15d911c2973f5d0 Mon Sep 17 00:00:00 2001 From: Sven Klemm Date: Wed, 1 Aug 2018 15:06:18 +0200 Subject: [PATCH 242/786] add compatibility code to handle pre 5.3 usage --- pkg/tsdb/postgres/macros.go | 17 +++++++++++++++++ pkg/tsdb/postgres/macros_test.go | 19 ++++++++++++++++--- 2 files changed, 33 insertions(+), 3 deletions(-) diff --git a/pkg/tsdb/postgres/macros.go b/pkg/tsdb/postgres/macros.go index fa887032c5d..9e337caf3ec 100644 --- a/pkg/tsdb/postgres/macros.go +++ b/pkg/tsdb/postgres/macros.go @@ -30,6 +30,23 @@ func (m *postgresMacroEngine) Interpolate(query *tsdb.Query, timeRange *tsdb.Tim var macroError error sql = replaceAllStringSubmatchFunc(rExp, sql, func(groups []string) string { + + // detect if $__timeGroup is supposed to add AS time for pre 5.3 compatibility + // if there is a ',' directly after the macro call $__timeGroup is probably used + // in the old way. Inside window function ORDER BY $__timeGroup will be followed + // by ')' + if groups[1] == "__timeGroup" { + if index := strings.Index(sql, groups[0]); index >= 0 { + index += len(groups[0]) + if len(sql) > index { + // check for character after macro expression + if sql[index] == ',' { + groups[1] = "__timeGroupAlias" + } + } + } + } + args := strings.Split(groups[2], ",") for i, arg := range args { args[i] = strings.Trim(arg, " ") diff --git a/pkg/tsdb/postgres/macros_test.go b/pkg/tsdb/postgres/macros_test.go index ec74470a803..beeea93893b 100644 --- a/pkg/tsdb/postgres/macros_test.go +++ b/pkg/tsdb/postgres/macros_test.go @@ -48,14 +48,27 @@ func TestMacroEngine(t *testing.T) { So(sql, ShouldEqual, fmt.Sprintf("select '%s'", from.Format(time.RFC3339))) }) + Convey("interpolate __timeGroup function pre 5.3 compatibility", func() { + + sql, err := engine.Interpolate(query, timeRange, "SELECT $__timeGroup(time_column,'5m'), value") + So(err, ShouldBeNil) + + So(sql, ShouldEqual, "SELECT floor(extract(epoch from time_column)/300)*300 AS \"time\", value") + + sql, err = engine.Interpolate(query, timeRange, "SELECT $__timeGroup(time_column,'5m') as time, value") + So(err, ShouldBeNil) + + So(sql, ShouldEqual, "SELECT floor(extract(epoch from time_column)/300)*300 as time, value") + }) + Convey("interpolate __timeGroup function", func() { - sql, err := engine.Interpolate(query, timeRange, "$__timeGroup(time_column,'5m')") + sql, err := engine.Interpolate(query, timeRange, "SELECT $__timeGroup(time_column,'5m')") So(err, ShouldBeNil) - sql2, err := engine.Interpolate(query, timeRange, "$__timeGroupAlias(time_column,'5m')") + sql2, err := engine.Interpolate(query, timeRange, "SELECT $__timeGroupAlias(time_column,'5m')") So(err, ShouldBeNil) - So(sql, ShouldEqual, "floor(extract(epoch from time_column)/300)*300") + So(sql, ShouldEqual, "SELECT floor(extract(epoch from time_column)/300)*300") So(sql2, ShouldEqual, sql+" AS \"time\"") }) From bb7e5838635fa044e75507c03827e4ba97cb7f53 Mon Sep 17 00:00:00 2001 From: Brice Maron Date: Wed, 1 Aug 2018 19:38:13 +0200 Subject: [PATCH 243/786] fix custom variable quoting in sql* query interpolations --- public/app/plugins/datasource/mssql/datasource.ts | 4 ++-- .../app/plugins/datasource/mssql/specs/datasource.jest.ts | 7 +++++++ public/app/plugins/datasource/mysql/datasource.ts | 4 ++-- .../app/plugins/datasource/mysql/specs/datasource.jest.ts | 7 +++++++ public/app/plugins/datasource/postgres/datasource.ts | 4 ++-- .../plugins/datasource/postgres/specs/datasource.jest.ts | 7 +++++++ 6 files changed, 27 insertions(+), 6 deletions(-) diff --git a/public/app/plugins/datasource/mssql/datasource.ts b/public/app/plugins/datasource/mssql/datasource.ts index 6656d4f96f7..dab7335ec97 100644 --- a/public/app/plugins/datasource/mssql/datasource.ts +++ b/public/app/plugins/datasource/mssql/datasource.ts @@ -16,7 +16,7 @@ export class MssqlDatasource { interpolateVariable(value, variable) { if (typeof value === 'string') { if (variable.multi || variable.includeAll) { - return "'" + value + "'"; + return "'" + value.replace(/'/g, `''`) + "'"; } else { return value; } @@ -31,7 +31,7 @@ export class MssqlDatasource { return value; } - return "'" + val + "'"; + return "'" + val.replace(/'/g, `''`) + "'"; }); return quotedValues.join(','); } diff --git a/public/app/plugins/datasource/mssql/specs/datasource.jest.ts b/public/app/plugins/datasource/mssql/specs/datasource.jest.ts index dd2d4a60cec..0308717775b 100644 --- a/public/app/plugins/datasource/mssql/specs/datasource.jest.ts +++ b/public/app/plugins/datasource/mssql/specs/datasource.jest.ts @@ -218,6 +218,13 @@ describe('MSSQLDatasource', function() { }); }); + describe('and variable contains single quote', () => { + it('should return a quoted value', () => { + ctx.variable.multi = true; + expect(ctx.ds.interpolateVariable("a'bc", ctx.variable)).toEqual("'a''bc'"); + }); + }); + describe('and variable allows all and value is a string', () => { it('should return a quoted value', () => { ctx.variable.includeAll = true; diff --git a/public/app/plugins/datasource/mysql/datasource.ts b/public/app/plugins/datasource/mysql/datasource.ts index 42fcf7b4564..67bb9d0a817 100644 --- a/public/app/plugins/datasource/mysql/datasource.ts +++ b/public/app/plugins/datasource/mysql/datasource.ts @@ -16,7 +16,7 @@ export class MysqlDatasource { interpolateVariable(value, variable) { if (typeof value === 'string') { if (variable.multi || variable.includeAll) { - return "'" + value + "'"; + return "'" + value.replace(/'/g, `''`) + "'"; } else { return value; } @@ -31,7 +31,7 @@ export class MysqlDatasource { return value; } - return "'" + val + "'"; + return "'" + val.replace(/'/g, `''`) + "'"; }); return quotedValues.join(','); } diff --git a/public/app/plugins/datasource/mysql/specs/datasource.jest.ts b/public/app/plugins/datasource/mysql/specs/datasource.jest.ts index be33f5f8858..85fa2b8cc4e 100644 --- a/public/app/plugins/datasource/mysql/specs/datasource.jest.ts +++ b/public/app/plugins/datasource/mysql/specs/datasource.jest.ts @@ -214,6 +214,13 @@ describe('MySQLDatasource', function() { }); }); + describe('and variable contains single quote', () => { + it('should return a quoted value', () => { + ctx.variable.multi = true; + expect(ctx.ds.interpolateVariable("a'bc", ctx.variable)).toEqual("'a''bc'"); + }); + }); + describe('and variable allows all and value is a string', () => { it('should return a quoted value', () => { ctx.variable.includeAll = true; diff --git a/public/app/plugins/datasource/postgres/datasource.ts b/public/app/plugins/datasource/postgres/datasource.ts index 8eee389d1a5..644c9e48b9b 100644 --- a/public/app/plugins/datasource/postgres/datasource.ts +++ b/public/app/plugins/datasource/postgres/datasource.ts @@ -16,7 +16,7 @@ export class PostgresDatasource { interpolateVariable(value, variable) { if (typeof value === 'string') { if (variable.multi || variable.includeAll) { - return "'" + value + "'"; + return "'" + value.replace(/'/g, `''`) + "'"; } else { return value; } @@ -27,7 +27,7 @@ export class PostgresDatasource { } var quotedValues = _.map(value, function(val) { - return "'" + val + "'"; + return "'" + val.replace(/'/g, `''`) + "'"; }); return quotedValues.join(','); } diff --git a/public/app/plugins/datasource/postgres/specs/datasource.jest.ts b/public/app/plugins/datasource/postgres/specs/datasource.jest.ts index 107cd76e6c5..cd6f57ee3fc 100644 --- a/public/app/plugins/datasource/postgres/specs/datasource.jest.ts +++ b/public/app/plugins/datasource/postgres/specs/datasource.jest.ts @@ -215,6 +215,13 @@ describe('PostgreSQLDatasource', function() { }); }); + describe('and variable contains single quote', () => { + it('should return a quoted value', () => { + ctx.variable.multi = true; + expect(ctx.ds.interpolateVariable("a'bc", ctx.variable)).toEqual("'a''bc'"); + }); + }); + describe('and variable allows all and is a string', () => { it('should return a quoted value', () => { ctx.variable.includeAll = true; From b71d10a7a42d9b47b191e981576cb17363f11a9d Mon Sep 17 00:00:00 2001 From: Sven Klemm Date: Wed, 1 Aug 2018 20:58:51 +0200 Subject: [PATCH 244/786] add $__timeGroupAlias to mysql and mssql --- pkg/tsdb/mssql/macros.go | 6 ++++++ pkg/tsdb/mssql/macros_test.go | 6 ++++++ pkg/tsdb/mysql/macros.go | 6 ++++++ pkg/tsdb/mysql/macros_test.go | 6 ++++++ 4 files changed, 24 insertions(+) diff --git a/pkg/tsdb/mssql/macros.go b/pkg/tsdb/mssql/macros.go index 2c16b5cb27f..f33ab1d40be 100644 --- a/pkg/tsdb/mssql/macros.go +++ b/pkg/tsdb/mssql/macros.go @@ -110,6 +110,12 @@ func (m *msSqlMacroEngine) evaluateMacro(name string, args []string) (string, er } } return fmt.Sprintf("FLOOR(DATEDIFF(second, '1970-01-01', %s)/%.0f)*%.0f", args[0], interval.Seconds(), interval.Seconds()), nil + case "__timeGroupAlias": + tg, err := m.evaluateMacro("__timeGroup", args) + if err == nil { + return tg + " AS [time]", err + } + return "", err case "__unixEpochFilter": if len(args) == 0 { return "", fmt.Errorf("missing time column argument for macro %v", name) diff --git a/pkg/tsdb/mssql/macros_test.go b/pkg/tsdb/mssql/macros_test.go index 1895cd99442..ea50c418de7 100644 --- a/pkg/tsdb/mssql/macros_test.go +++ b/pkg/tsdb/mssql/macros_test.go @@ -55,15 +55,21 @@ func TestMacroEngine(t *testing.T) { Convey("interpolate __timeGroup function", func() { sql, err := engine.Interpolate(query, timeRange, "GROUP BY $__timeGroup(time_column,'5m')") So(err, ShouldBeNil) + sql2, err := engine.Interpolate(query, timeRange, "GROUP BY $__timeGroupAlias(time_column,'5m')") + So(err, ShouldBeNil) So(sql, ShouldEqual, "GROUP BY FLOOR(DATEDIFF(second, '1970-01-01', time_column)/300)*300") + So(sql2, ShouldEqual, sql+" AS [time]") }) Convey("interpolate __timeGroup function with spaces around arguments", func() { sql, err := engine.Interpolate(query, timeRange, "GROUP BY $__timeGroup(time_column , '5m')") So(err, ShouldBeNil) + sql2, err := engine.Interpolate(query, timeRange, "GROUP BY $__timeGroupAlias(time_column , '5m')") + So(err, ShouldBeNil) So(sql, ShouldEqual, "GROUP BY FLOOR(DATEDIFF(second, '1970-01-01', time_column)/300)*300") + So(sql2, ShouldEqual, sql+" AS [time]") }) Convey("interpolate __timeGroup function with fill (value = NULL)", func() { diff --git a/pkg/tsdb/mysql/macros.go b/pkg/tsdb/mysql/macros.go index 078d1ff54f8..a56fd1ceb2a 100644 --- a/pkg/tsdb/mysql/macros.go +++ b/pkg/tsdb/mysql/macros.go @@ -105,6 +105,12 @@ func (m *mySqlMacroEngine) evaluateMacro(name string, args []string) (string, er } } return fmt.Sprintf("UNIX_TIMESTAMP(%s) DIV %.0f * %.0f", args[0], interval.Seconds(), interval.Seconds()), nil + case "__timeGroupAlias": + tg, err := m.evaluateMacro("__timeGroup", args) + if err == nil { + return tg + " AS \"time\"", err + } + return "", err case "__unixEpochFilter": if len(args) == 0 { return "", fmt.Errorf("missing time column argument for macro %v", name) diff --git a/pkg/tsdb/mysql/macros_test.go b/pkg/tsdb/mysql/macros_test.go index 003af9a737f..fd9d3f5688a 100644 --- a/pkg/tsdb/mysql/macros_test.go +++ b/pkg/tsdb/mysql/macros_test.go @@ -38,16 +38,22 @@ func TestMacroEngine(t *testing.T) { sql, err := engine.Interpolate(query, timeRange, "GROUP BY $__timeGroup(time_column,'5m')") So(err, ShouldBeNil) + sql2, err := engine.Interpolate(query, timeRange, "GROUP BY $__timeGroupAlias(time_column,'5m')") + So(err, ShouldBeNil) So(sql, ShouldEqual, "GROUP BY UNIX_TIMESTAMP(time_column) DIV 300 * 300") + So(sql2, ShouldEqual, sql+" AS \"time\"") }) Convey("interpolate __timeGroup function with spaces around arguments", func() { sql, err := engine.Interpolate(query, timeRange, "GROUP BY $__timeGroup(time_column , '5m')") So(err, ShouldBeNil) + sql2, err := engine.Interpolate(query, timeRange, "GROUP BY $__timeGroupAlias(time_column , '5m')") + So(err, ShouldBeNil) So(sql, ShouldEqual, "GROUP BY UNIX_TIMESTAMP(time_column) DIV 300 * 300") + So(sql2, ShouldEqual, sql+" AS \"time\"") }) Convey("interpolate __timeFilter function", func() { From 82c473e3af4800a8cb9f20c96530190b6c44d847 Mon Sep 17 00:00:00 2001 From: Sven Klemm Date: Wed, 1 Aug 2018 21:23:00 +0200 Subject: [PATCH 245/786] document $__timeGroupAlias --- docs/sources/features/datasources/mssql.md | 1 + docs/sources/features/datasources/mysql.md | 1 + docs/sources/features/datasources/postgres.md | 1 + public/app/plugins/datasource/mssql/partials/query.editor.html | 1 + public/app/plugins/datasource/mysql/partials/query.editor.html | 1 + .../app/plugins/datasource/postgres/partials/query.editor.html | 1 + 6 files changed, 6 insertions(+) diff --git a/docs/sources/features/datasources/mssql.md b/docs/sources/features/datasources/mssql.md index ea7be8e1c30..dabb896ec0f 100644 --- a/docs/sources/features/datasources/mssql.md +++ b/docs/sources/features/datasources/mssql.md @@ -82,6 +82,7 @@ Macro example | Description *$__timeTo()* | Will be replaced by the end of the currently active time selection. For example, *'2017-04-21T05:06:17Z'* *$__timeGroup(dateColumn,'5m'[, fillvalue])* | Will be replaced by an expression usable in GROUP BY clause. Providing a *fillValue* of *NULL* or *floating value* will automatically fill empty series in timerange with that value.
    For example, *CAST(ROUND(DATEDIFF(second, '1970-01-01', time_column)/300.0, 0) as bigint)\*300*. *$__timeGroup(dateColumn,'5m', 0)* | Same as above but with a fill parameter so all null values will be converted to the fill value (all null values would be set to zero using this example). +*$__timeGroupAlias(dateColumn,'5m')* | Will be replaced identical to $__timeGroup but with an added column alias (only available in Grafana 5.3+). *$__unixEpochFilter(dateColumn)* | Will be replaced by a time range filter using the specified column name with times represented as unix timestamp. For example, *dateColumn > 1494410783 AND dateColumn < 1494497183* *$__unixEpochFrom()* | Will be replaced by the start of the currently active time selection as unix timestamp. For example, *1494410783* *$__unixEpochTo()* | Will be replaced by the end of the currently active time selection as unix timestamp. For example, *1494497183* diff --git a/docs/sources/features/datasources/mysql.md b/docs/sources/features/datasources/mysql.md index 22287b2a838..a0e67037005 100644 --- a/docs/sources/features/datasources/mysql.md +++ b/docs/sources/features/datasources/mysql.md @@ -65,6 +65,7 @@ Macro example | Description *$__timeTo()* | Will be replaced by the end of the currently active time selection. For example, *'2017-04-21T05:06:17Z'* *$__timeGroup(dateColumn,'5m')* | Will be replaced by an expression usable in GROUP BY clause. For example, *cast(cast(UNIX_TIMESTAMP(dateColumn)/(300) as signed)*300 as signed),* *$__timeGroup(dateColumn,'5m',0)* | Same as above but with a fill parameter so all null values will be converted to the fill value (all null values would be set to zero using this example). +*$__timeGroupAlias(dateColumn,'5m')* | Will be replaced identical to $__timeGroup but with an added column alias (only available in Grafana 5.3+). *$__unixEpochFilter(dateColumn)* | Will be replaced by a time range filter using the specified column name with times represented as unix timestamp. For example, *dateColumn > 1494410783 AND dateColumn < 1494497183* *$__unixEpochFrom()* | Will be replaced by the start of the currently active time selection as unix timestamp. For example, *1494410783* *$__unixEpochTo()* | Will be replaced by the end of the currently active time selection as unix timestamp. For example, *1494497183* diff --git a/docs/sources/features/datasources/postgres.md b/docs/sources/features/datasources/postgres.md index 7915f29fcdc..35dfcac15c0 100644 --- a/docs/sources/features/datasources/postgres.md +++ b/docs/sources/features/datasources/postgres.md @@ -62,6 +62,7 @@ Macro example | Description *$__timeTo()* | Will be replaced by the end of the currently active time selection. For example, *'2017-04-21T05:06:17Z'* *$__timeGroup(dateColumn,'5m')* | Will be replaced by an expression usable in GROUP BY clause. For example, *(extract(epoch from dateColumn)/300)::bigint*300* *$__timeGroup(dateColumn,'5m', 0)* | Same as above but with a fill parameter so all null values will be converted to the fill value (all null values would be set to zero using this example). +*$__timeGroupAlias(dateColumn,'5m')* | Will be replaced identical to $__timeGroup but with an added column alias (only available in Grafana 5.3+). *$__unixEpochFilter(dateColumn)* | Will be replaced by a time range filter using the specified column name with times represented as unix timestamp. For example, *dateColumn >= 1494410783 AND dateColumn <= 1494497183* *$__unixEpochFrom()* | Will be replaced by the start of the currently active time selection as unix timestamp. For example, *1494410783* *$__unixEpochTo()* | Will be replaced by the end of the currently active time selection as unix timestamp. For example, *1494497183* diff --git a/public/app/plugins/datasource/mssql/partials/query.editor.html b/public/app/plugins/datasource/mssql/partials/query.editor.html index 397a35164c0..e1320aabde2 100644 --- a/public/app/plugins/datasource/mssql/partials/query.editor.html +++ b/public/app/plugins/datasource/mssql/partials/query.editor.html @@ -54,6 +54,7 @@ Macros: - $__timeFilter(column) -> column BETWEEN '2017-04-21T05:01:17Z' AND '2017-04-21T05:01:17Z' - $__unixEpochFilter(column) -> column >= 1492750877 AND column <= 1492750877 - $__timeGroup(column, '5m'[, fillvalue]) -> CAST(ROUND(DATEDIFF(second, '1970-01-01', column)/300.0, 0) as bigint)*300. Providing a fillValue of NULL or floating value will automatically fill empty series in timerange with that value. +- $__timeGroupAlias(column, '5m'[, fillvalue]) -> CAST(ROUND(DATEDIFF(second, '1970-01-01', column)/300.0, 0) as bigint)*300 AS [time] Example of group by and order by with $__timeGroup: SELECT diff --git a/public/app/plugins/datasource/mysql/partials/query.editor.html b/public/app/plugins/datasource/mysql/partials/query.editor.html index d4be22fc3e9..db12a3fe8ce 100644 --- a/public/app/plugins/datasource/mysql/partials/query.editor.html +++ b/public/app/plugins/datasource/mysql/partials/query.editor.html @@ -54,6 +54,7 @@ Macros: - $__timeFilter(column) -> column BETWEEN '2017-04-21T05:01:17Z' AND '2017-04-21T05:01:17Z' - $__unixEpochFilter(column) -> time_unix_epoch > 1492750877 AND time_unix_epoch < 1492750877 - $__timeGroup(column,'5m') -> cast(cast(UNIX_TIMESTAMP(column)/(300) as signed)*300 as signed) +- $__timeGroupAlias(column,'5m') -> cast(cast(UNIX_TIMESTAMP(column)/(300) as signed)*300 as signed) AS "time" Example of group by and order by with $__timeGroup: SELECT diff --git a/public/app/plugins/datasource/postgres/partials/query.editor.html b/public/app/plugins/datasource/postgres/partials/query.editor.html index 1ace05abae2..1b7278f6809 100644 --- a/public/app/plugins/datasource/postgres/partials/query.editor.html +++ b/public/app/plugins/datasource/postgres/partials/query.editor.html @@ -54,6 +54,7 @@ Macros: - $__timeFilter(column) -> column BETWEEN '2017-04-21T05:01:17Z' AND '2017-04-21T05:01:17Z' - $__unixEpochFilter(column) -> column >= 1492750877 AND column <= 1492750877 - $__timeGroup(column,'5m') -> (extract(epoch from column)/300)::bigint*300 +- $__timeGroupAlias(column,'5m') -> (extract(epoch from column)/300)::bigint*300 AS "time" Example of group by and order by with $__timeGroup: SELECT From a221d9ec84edad72f8fd1e1530a1b4efe2104bbe Mon Sep 17 00:00:00 2001 From: Sven Klemm Date: Thu, 2 Aug 2018 09:50:21 +0200 Subject: [PATCH 246/786] add more prominent button for switching edit mode --- .../plugins/datasource/postgres/partials/query.editor.html | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/public/app/plugins/datasource/postgres/partials/query.editor.html b/public/app/plugins/datasource/postgres/partials/query.editor.html index d32e84f30db..218dd306985 100644 --- a/public/app/plugins/datasource/postgres/partials/query.editor.html +++ b/public/app/plugins/datasource/postgres/partials/query.editor.html @@ -105,6 +105,12 @@ +
    + +
    diff --git a/public/sass/components/_panel_table.scss b/public/sass/components/_panel_table.scss index 99e91f8ff67..c793cd408b6 100644 --- a/public/sass/components/_panel_table.scss +++ b/public/sass/components/_panel_table.scss @@ -87,6 +87,12 @@ height: 100%; display: inline-block; } + + &.white { + a { + color: white; + } + } } &.cell-highlighted:hover { From b03e3242e3ee4092ad7cc81219d35a39a2cd6c40 Mon Sep 17 00:00:00 2001 From: Patrick O'Carroll Date: Thu, 2 Aug 2018 11:21:17 +0200 Subject: [PATCH 249/786] removed table-panel-link class --- public/sass/components/_panel_table.scss | 4 ---- 1 file changed, 4 deletions(-) diff --git a/public/sass/components/_panel_table.scss b/public/sass/components/_panel_table.scss index c793cd408b6..fc14236c2b7 100644 --- a/public/sass/components/_panel_table.scss +++ b/public/sass/components/_panel_table.scss @@ -139,7 +139,3 @@ height: 0px; line-height: 0px; } - -.table-panel-link { - color: white; -} From a8976f6c36005fcbec485f001766560b0767f45f Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Thu, 2 Aug 2018 11:43:48 +0200 Subject: [PATCH 250/786] changelog: add notes about closing #12785 [skip ci] --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index aa089b5900b..66ab1906c4d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ * **Variables**: Skip unneeded extra query request when de-selecting variable values used for repeated panels [#8186](https://github.com/grafana/grafana/issues/8186), thx [@mtanda](https://github.com/mtanda) * **Postgres/MySQL/MSSQL**: Use floor rounding in $__timeGroup macro function [#12460](https://github.com/grafana/grafana/issues/12460), thx [@svenklemm](https://github.com/svenklemm) * **Postgres/MySQL/MSSQL**: Use metric column as prefix when returning multiple value columns [#12727](https://github.com/grafana/grafana/issues/12727), thx [@svenklemm](https://github.com/svenklemm) +* **Postgres/MySQL/MSSQL**: Escape single quotes in variables [#12785](https://github.com/grafana/grafana/issues/12785), thx [@eMerzh](https://github.com/eMerzh) * **MySQL/MSSQL**: Use datetime format instead of epoch for $__timeFilter, $__timeFrom and $__timeTo macros [#11618](https://github.com/grafana/grafana/issues/11618) [#11619](https://github.com/grafana/grafana/issues/11619), thx [@AustinWinstanley](https://github.com/AustinWinstanley) * **Postgres**: Escape ssl mode parameter in connectionstring [#12644](https://github.com/grafana/grafana/issues/12644), thx [@yogyrahmawan](https://github.com/yogyrahmawan) * **Github OAuth**: Allow changes of user info at Github to be synched to Grafana when signing in [#11818](https://github.com/grafana/grafana/issues/11818), thx [@rwaweber](https://github.com/rwaweber) From 04fcd2a05481c799176420e802bd73ec24d699a0 Mon Sep 17 00:00:00 2001 From: Mitsuhiro Tanda Date: Thu, 2 Aug 2018 18:49:40 +0900 Subject: [PATCH 251/786] add series override option to hide tooltip (#12378) * add series override option to hide tooltip * fix test * invert option * fix test * remove initialization --- public/app/core/time_series2.ts | 4 ++++ public/app/plugins/panel/graph/graph_tooltip.ts | 5 +++++ .../app/plugins/panel/graph/series_overrides_ctrl.ts | 1 + .../plugins/panel/graph/specs/graph_tooltip.jest.ts | 11 ++++++++++- 4 files changed, 20 insertions(+), 1 deletion(-) diff --git a/public/app/core/time_series2.ts b/public/app/core/time_series2.ts index 59729ebc312..f4d0943d52f 100644 --- a/public/app/core/time_series2.ts +++ b/public/app/core/time_series2.ts @@ -76,6 +76,7 @@ export default class TimeSeries { valueFormater: any; stats: any; legend: boolean; + hideTooltip: boolean; allIsNull: boolean; allIsZero: boolean; decimals: number; @@ -181,6 +182,9 @@ export default class TimeSeries { if (override.legend !== void 0) { this.legend = override.legend; } + if (override.hideTooltip !== void 0) { + this.hideTooltip = override.hideTooltip; + } if (override.yaxis !== void 0) { this.yaxis = override.yaxis; diff --git a/public/app/plugins/panel/graph/graph_tooltip.ts b/public/app/plugins/panel/graph/graph_tooltip.ts index 509d15b8a25..7bbafc453eb 100644 --- a/public/app/plugins/panel/graph/graph_tooltip.ts +++ b/public/app/plugins/panel/graph/graph_tooltip.ts @@ -81,6 +81,11 @@ export default function GraphTooltip(elem, dashboard, scope, getSeriesFn) { continue; } + if (series.hideTooltip) { + results[0].push({ hidden: true, value: 0 }); + continue; + } + hoverIndex = this.findHoverIndexFromData(pos.x, series); hoverDistance = pos.x - series.data[hoverIndex][0]; pointTime = series.data[hoverIndex][0]; diff --git a/public/app/plugins/panel/graph/series_overrides_ctrl.ts b/public/app/plugins/panel/graph/series_overrides_ctrl.ts index 5958c80bac9..024c9cac93b 100644 --- a/public/app/plugins/panel/graph/series_overrides_ctrl.ts +++ b/public/app/plugins/panel/graph/series_overrides_ctrl.ts @@ -152,6 +152,7 @@ export function SeriesOverridesCtrl($scope, $element, popoverSrv) { $scope.addOverrideOption('Z-index', 'zindex', [-3, -2, -1, 0, 1, 2, 3]); $scope.addOverrideOption('Transform', 'transform', ['negative-Y']); $scope.addOverrideOption('Legend', 'legend', [true, false]); + $scope.addOverrideOption('Hide in tooltip', 'hideTooltip', [true, false]); $scope.updateCurrentOverrides(); } diff --git a/public/app/plugins/panel/graph/specs/graph_tooltip.jest.ts b/public/app/plugins/panel/graph/specs/graph_tooltip.jest.ts index 3bc60ed8ea3..baebf2c5930 100644 --- a/public/app/plugins/panel/graph/specs/graph_tooltip.jest.ts +++ b/public/app/plugins/panel/graph/specs/graph_tooltip.jest.ts @@ -68,7 +68,10 @@ describe('findHoverIndexFromData', function() { describeSharedTooltip('steppedLine false, stack false', function(ctx) { ctx.setup(function() { - ctx.data = [{ data: [[10, 15], [12, 20]], lines: {} }, { data: [[10, 2], [12, 3]], lines: {} }]; + ctx.data = [ + { data: [[10, 15], [12, 20]], lines: {}, hideTooltip: false }, + { data: [[10, 2], [12, 3]], lines: {}, hideTooltip: false }, + ]; ctx.pos = { x: 11 }; }); @@ -105,6 +108,7 @@ describeSharedTooltip('steppedLine false, stack true, individual false', functio points: [[10, 15], [12, 20]], }, stack: true, + hideTooltip: false, }, { data: [[10, 2], [12, 3]], @@ -114,6 +118,7 @@ describeSharedTooltip('steppedLine false, stack true, individual false', functio points: [[10, 2], [12, 3]], }, stack: true, + hideTooltip: false, }, ]; ctx.ctrl.panel.stack = true; @@ -136,6 +141,7 @@ describeSharedTooltip('steppedLine false, stack true, individual false, series s points: [[10, 15], [12, 20]], }, stack: true, + hideTooltip: false, }, { data: [[10, 2], [12, 3]], @@ -145,6 +151,7 @@ describeSharedTooltip('steppedLine false, stack true, individual false, series s points: [[10, 2], [12, 3]], }, stack: false, + hideTooltip: false, }, ]; ctx.ctrl.panel.stack = true; @@ -167,6 +174,7 @@ describeSharedTooltip('steppedLine false, stack true, individual true', function points: [[10, 15], [12, 20]], }, stack: true, + hideTooltip: false, }, { data: [[10, 2], [12, 3]], @@ -176,6 +184,7 @@ describeSharedTooltip('steppedLine false, stack true, individual true', function points: [[10, 2], [12, 3]], }, stack: false, + hideTooltip: false, }, ]; ctx.ctrl.panel.stack = true; From 169fcba52031b104a39b1442edf655e9372541f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Thu, 2 Aug 2018 11:51:41 +0200 Subject: [PATCH 252/786] Update CHANGELOG.md --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 66ab1906c4d..22c24f83b7e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -31,6 +31,8 @@ * **Elasticsearch**: For alerting/backend, support having index name to the right of pattern in index pattern [#12731](https://github.com/grafana/grafana/issues/12731) * **OAuth**: Fix overriding tls_skip_verify_insecure using environment variable [#12747](https://github.com/grafana/grafana/issues/12747), thx [@jangaraj](https://github.com/jangaraj) * **Units**: Change units to include characters for power of 2 and 3 [#12744](https://github.com/grafana/grafana/pull/12744), thx [@Worty](https://github.com/Worty) +* **Graph**: Option to hide series from tooltip [#3341](https://github.com/grafana/grafana/pull/3341), thx [@mtanda](https://github.com/mtanda) + # 5.2.2 (2018-07-25) From 57910549b6b8eb639c6cd36814d3a0850a123bf2 Mon Sep 17 00:00:00 2001 From: andig Date: Thu, 2 Aug 2018 12:37:50 +0200 Subject: [PATCH 253/786] Improve iOS and Windows 10 experience (#12769) * Improve iOS homescreen icon * Improve Windows10 tile experience * Remove unused favicon --- public/img/apple-touch-icon.png | Bin 0 -> 15718 bytes public/img/browserconfig.xml | 9 +++++++++ public/img/mstile-150x150.png | Bin 0 -> 9010 bytes public/views/index.template.html | 7 +++++-- 4 files changed, 14 insertions(+), 2 deletions(-) create mode 100644 public/img/apple-touch-icon.png create mode 100644 public/img/browserconfig.xml create mode 100644 public/img/mstile-150x150.png diff --git a/public/img/apple-touch-icon.png b/public/img/apple-touch-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..3031d9aa011fb298c06563370d6b669c0d0232c2 GIT binary patch literal 15718 zcmZ`=V{~OrvyN@swry*YiS0~mp5(-t*v7=3iS6XXwr$%JfAjvlKkn+?U2At&)o!ou zXIDjjSC&OaAV2^E14EXVlT!Of8~=-NF#l5FNf?fQ1jZL)T|DBuDfGX0GIKe# zZ(v|PG+uW>a|-^_Z6#9` zlSYDL%8;WFhepSe9&~RrfQ0$3%HE9-R$(NC{)ll9g)1$_QD`Ej@ja2u-o(qwEo)^{ zCFeOlH}~|$JtOlflQ?tx`2Ip!R7EpKplnKAW%0Lq*#H*})_GjEia?iPWLO)l8IhA^ z3)Bs5vhT5B1x<&KI!V?Ag3zzpr;))x0*GOHIZ~LXK>)&Eo;}z;9*-^ls)haDMhO+L zCDJA=(~RGUx8MWoK}*?G@;fLKBA6K;Q!39(=0lAV%lMnJOj40hY0B4(ZPS!-(7_`E zL-S#NLopRWK`&k^jE~m4G#wCMV}REi{zh&l(A?)>QU`_$1o~KIqG+0Y`-D6${N$O4 z?Wsir+acSg)Bt18m`s-h3F39V-T*GMSD%k6~jU_LU2Zza=(?3EWNE@953{ha0>cQz%} zKmT!&x5w)RXEu?u$Pj5UlKI^} z(6{MBMN_>3yTFO>{ay%$VD7Oz0n(2PH(#tC%GXIRCLec$$&*hkkv5?uv`a)?8T}pF zy9ZNJMEq$Se=eJ#QBEgYchI;t6=nY^8IA-I^_=Oo^WJ!prM*=qjbRJd!#wc;-Hha{A8_=dlx2Bap86G>Or?FqGu;SdNy1vhMxuch2`w&?p zL9}OCUWEb-uRau#du%I-4{;39h(V_^c=`bg4>|(4^9aH+l|G|pd&uAtv=ANfG}jqb z#BDDF6;ict?wCjBdIaWDk|C7bJI4?;1q%Fe{V2qt)Sx@EBOSK^ch($f`*&#}#a++@ zO47qd`LL?_N!`;Jp7g%mcqLi4pwe5l&JKxjz`~qnSQWd|CJHZ{<^9#~SjqX)BHTo0 zdYHa25YSHiOl$&ULdtdk4vf>@Ztb1)XAB!-La2MU7o566!t+ z7_YE|&;=)1iK`>2gZLYrDGc?_jN}mdyoAfQ8Vsz^uD_zh4dK56hb}q29bb> zVt59S^1+=Yk?grHCZVpheBMsm1N;mRjOEkj%OT<~;qRYTqk}F5A%jNvZOmZtyb>dKF?Deo@A5DiOAt7-L7u(@?*|=5bd79t zXb}vlFg-ZQoL&Bz#Wic>$dbW#?{P=R$x>HmGu|r!F^Y#%q=9oP9F8M+Vj=JeHI|FV z6Kl$#;varu?|I%o!E&yj{0xOV1`%>XxTg%P!pMR(42$_NT9yfvb&!01z{G9Mihj!A zQE$)FfzA%2b-D1Y9E4VhpsvUYFayoQe18!#9Fl4s1=l$}tpZX#gdW0Y5Z{R#ej#sO zOn#mby&>kH62UgcR6q;`l)(0X!R@gK&vrdDN*w<;M+XRg$)i3^gU7+-h#bHO@bYc~ zQtPv}I*djfZpdFZ@dEJm@Ybrb-m%Xe-uq+B1F;RgYO=q7mQH$tl36EP&3%3|a<2rS zoY2Z?Qy3`*arFPib`kPCx$UDX+ztg`Ytp$bc1Hie2xkbq!kx(b{+YXI+3_gL{DX~K zjG{E|swgd+?ty3Eo^JP3H)8f_q3I%1R$r^C2LXG`4$KGxh30FyX`qiFu@a#pMMBhV z0D|cfo63xx78iBWEEpy{3mq9x;)p%T@_~XEu6QE+x6~J>X?t5_HIP9?B3HLNrIlC0 ziIZ?Ib{PCxhDZT>f8RhO>jt~@cO7v%6T$-nmk zv;^!P6i;XuG9ZrPP|$nE0yr1AOAH=L5K^L+WyP1WiLm&M(n|eubv5nXNwuIYJsvnXD_rq?AZB1@(~(o7aEqn_+gLrVogAl$l2gl}V4>BB@=Le3iwG&O8LYw}AC~-O{pwKBpOm0t zt^nrOUfA$-vOCIL8>sw=V{u%9JjmgN$qOm*b~%4-k64<3K&m3 zMbd1%d&5ni6DtlEDvU5AG8c|LJdW{FpAck{W_~$v8*%HM9~rS4bE5}=8N2&r&mF?23N{fd`vG`=F>iSLKZA66OS|1Ua#E6+-ygGXYdg_(z-v!WeiSE#ObwcU zBeBC>Lv+bk3J05(WvcI+gy-H9E=w3p>bBSu2Ct)CB5sO!!OUOqcp<42a3_ha`lfR- zEz$mmXm^xIWFhktWyfVl+wxsuB`*kh2o#tw*(zayejFGNPqhw2b+>Sn|yZPp^xMc zryB4EkG_BFH92Ed#wVYbr7?WwHbOlpgl=C!5C)&%2J-&X*ThbnSdwT#r6Ar2Vn|$$ z7aZ|?NE~|TOsIGj+_M9JoA*K_eWh))&$8XoSa9|tj?gYn?`O+*@Y^Xd0hn9M9cc?D znj#7K)iSoX0Dwp+7h8?8e#2Sm%%&_egiVlh3;5e&v@(_?CL0SWs1^5bnl7MBQBP&% zBqQMX080BeH4l+UUUz5}m%~Y3hzrpizXg_{5xo<3{dVMP08R3G4`DR->N~LA&giS1 z{voRfm(m)=xdkkIxxO!%FOiFrg!}KGOvCLwqK&n=1*KtP6&1DEv+%NXRIwgST zeeRDJrJ z!j9N%JHtaX36v8_KMBn~4ZmZ|V{Hz@O!%#Lo^k-Qv$*Rgm2`eyR7SwG?ay<0+IeHH z2E?GrA_lQeI6k+nJMFvaxiI62af$}YojO*sqN_igU}cm4`q|F7^hOn zfJ_7X+T3oKuRs4OGP{++xb6o`U5Rhnbaq~kR3sK&0E5vtIQW0ITLuOQgw#HZYubRK}F)q-oz3rrLJ zqeYPYgZ*%4hxQ)P7JoIkh{V)Qe@Y9UK9Y-`Bt=~S1zv}qvK;8ASguZ?OUI~x6x=MD zn{HK9>zMF(7nNqJBh`o83yl^ysap`x_IM~V`s%(WZc0!)>EUnhjKB4b&49AzadyYNp;7P2@l*J&VVRKYKtmeA$AF=>cnhHGInXu32b);5t9F<@WqSV zH5UVO6<5X(}{pB1+8fBzu3Z_;+?jWv{$uH6V?(iGQdRA*M*DD)T zdD=;9`&}nil8>occ=eO&b!|?{EwhO81Yx5UNYt7yR7N6nr-u5mh*4Lc^@4GD{=)-W z*%xjKp$nIZ?w>_gTbJ|6AfLkXCrv1plf+w_T4OBF?>LK7amm0WLEH%&;tz5IOyE!y zlH7;njcAgu)+iNux@P-@FyZWWp(6D)d-k&H7~v^46(Ll&fY)qH`1~pS;iqo&)Vj7= zkO$$X#7<(TC$9G;*TNMn->DpLSO&nuLpiE%NShVh+-pDu4farE{C zN(r^>vG6%fY`9h2g@ueA;%|#PEcx_gHyAUE4#H(%o`|BE$rfwH_8iYHLf7shB0PZ} zTyod3+^fKkr^;0B+nCE298N*V)1|3jUYi9n&lR|dswTYi^5f3UH73R;PA0z#Jj3G7 zzE5c{fe`0lLQJ|qFa@cW@AI{^y}59)*z65Cr$b@21vT;fEzACd8uXO`ix5bpUM!z3 z@}#9wT!Asc>v|J$n6GZb%!^(^nAJ02=kdBf@g1hxt`)K&Bzt=suo)Jmj~4{as-_ej z2Y+O)Y8HeZBOtqP*4y!)c(Gph(Z=f7RU)^A{ETayu&D-u=ZsyQj_U&ci~t>d%oAe1 z0kBs{(NHs5M2jbD6iP5hH?MTq$$hIuE!i!X@cEu02P*2LjnOY1!~Dcf7>SEcd}*W$ zmPxuIbc)!f$z9uJGDkjVo1(s-s0q8R%7i)IluCLv*mqRhj<$nJlKf)p2EHb z^Jazyu=UtYKrNGWBrMikxCS-KQaF<)@6%YM!^g>Fh0a)evs%?mE1rufBfG9i#&J@= zktp|&ndY&r2HC~y;~4|rgX`+G&_fF9C^NfJS6dQ76CJL>PRR;{WiZ+N)O@T1n`(5# zI{87JIn<{Jl_tM@Gpn05s_sRM`JbT(Coi!0HjdriMldN1BF1c_fo|Gq?%-q>0aiH)I zL9Jdu>_q@&H%S1;zR30P5h0sVrKk-!M>uAO;H_q{3MG!#o~hEi)fbayX+?K-K_8Ks zsC+vAuorc1&+cP%Jb_Eabd8v;pvg2ou6s$z{jN%t zu*5D=j_!s;#X{=|!wixA={lHJY|f((7c*~zfH{ag@uaGdW&>sI9X=*b8BKA&^8ujI zDppDf`)vAr&*-ftzlixtXKWeE-f_baZ5bYTB&Y;bE!FJ!GtYDld@nFf=5|K)p7DHX zfae}gGZK=@6O&(r@ZeMQARas-yUAa}SB1>}WNdPp^rx^^St~Jh-E9*h;G{WWC~TnO z=cm52SnhS7vth+Sj%z1l)^ouZem5?8MK=5%n_p_OFx({8>ocg$$^0ohooID1Og%kD zSIdyMTBZQ@`isiN4eMI@qOM3ZyRN*dzFoOh!Yi*cPzNn=-$V*%IiZ0D%(t#g-z-Zy z*G<32FJTiV6WY@nT-bK$AE-ib1o9i(F@gPU^kC$k8c?5QeDQnSRI0 z!XA^K;3@wiJuWh@4;!(p@LG*(7XGu2@0T+EFdCI39Z50L8>xvK`n6dkgk?q4_lti! zrZ^aVo?`+)JaLuIOAYNuJHw-w(dTZMfn7c91WmJ-r#NNlO&dl!1TdAWSF>L*CieNF zOTjg!u!h4s%BDvnZrt4e=Eo(=WY}`NL;cEavLh2F@s8KXRb;;Kwuu%uPF5tTtA&BI z6(#X^wbs?QP-H!Yzik^xu{}ypa@L=t36Tws_JDqf&N)Mnl?#!M5VUmufQ!HBkPCjkd?w;JM2wwiO=^Oe8t&|p1% zx7x+W$3k)P8jBw)?vp_4|4A;*`BQ6-&8{I*qf`MMazwFmvqwrF7s5*SHBbWB;R8dj_%2GW6^lFR5hk8 zG)Y@9ffN^RVVxTej^mOE>DDN>{iJ$r|Bs>{0c3lOfUjmTAN#}eqq>Jz)!c_elPkrv z>h%ZEA7i+2RqH`C#6Owq|0>8k6~VcZ=NT!}!khXQWU3pNFM*YnD5UahbszJ`W(K}- zAguw_gLr-1dC8eh_Z9h#S4hR=#c}RUBy5w2H<|-eyCcvd%v;nb4_nFh1fYl@97_KOO(C#}t~ZVkN;DTmeUsk!S47%j?NjXiN}uOs z!KFP1c@@L5FhK=-x4EV0#pCbPN%4vH>d~`-r$30`C++?YS_Hy`TWnvbmY?DUj0u9u zd-glf8fq7=#wi@es=e^y`DLA}!#vsDCi(YSXT-mrI$aP6RrJC3&{{Nixy%)6-}Bik-^K?+#d>w8*1!Pg*PR(#ZG)&X=QWgY26w z;sJ=6C3}03hTe7(5v_w3PI7BI)CGvz7f<`KOWxlcniV~aTH>b8+nL?~ z5z$nzM!RO#FsR?uY-Dm_<($&zZHSjEf6Skz;!3*iLKg9-ov^#e25n@aY`Q6T7D(U< z6(C#^@mV3FT_y%kG*!q zDtjc>R+)fVnj)>HWV50H4wzpQopI9-f7i15-F~;f4=$2Ge)Ae^8>9>vQbBT$zQo=oPyvAWisI9n*T{TzIBnRD9*_*AO4bOTUZ4<#~ zR>NC!RC~xU8=^u~7{YGK0%ZI%f;v3cKj|JyIR(KD&fKivSLa>`Edh6=y7)Hb2&?qvztIhEc?Twv?V0@s^2-g$90Tbt~+{#}|e zt)_N+IoG6^0CB-Ph_3HcMKe-@1-K6b-d!$J2Ss#FTTpXa+ymxX)AyDg(^qDmzBH~x zu^GC2LB!R-x|%^!F=pN~8r@h~5iid|kMEe4^nptj3 zL3?lVH!lq57AzK1>bO+`cut=_#-YZ_9?^tF15wEsjb84F%GCPQIVb}0N(m{Z zUrB|3uvhHE=QT~oC*|!jx};hDif0fM@BfK?G>QLaH4U4=c={EXZd;DA`mLRM2GTRG z;QYwn=?-{AR~g`jn{)qbrQUzXz1WFjts=?q#}@dT#LhhFBg>IhK#f1E+a&O$Cz~h! z%g2}Hl+7bQyF0`k-;7iy+q;N0;;Mi?hgQ&nP~OgFfQHgS*7##+CX>&XN&G@sDd4ZE z{HOn}FEI5T73qHRTMd|Fo9MJt_BLko>+NS+Us+6kqApJc zM*|o?kJcehg?4;vpVTT!GvBoK{GVhN*j6Y$$NI@K#g07s$L7}~1|ii>AG%F$U2WXX z04*&ypQ9*6-!oLShA+Xjh2nzbl&&3*Svjvx%y zshn z%aVe9VrE8Z*}%UNDfRpJA}O*Ih{5R`Q@ay_cHa7*nzXHAP`4&DW}j>y5$!hR{xqMF z7rbFsQ(iRFM?!k;26aI?LTMZ>&}N#d`WicivzJ>;W&1D^hz2<{nEPrc%*Ru-4|)t` zHHz63yLUbZ617*u0tCKsct&K{Tv`SaC(U{!U;(q<&sp~4 zOoO)b)^$V)=EE4tU(mH0nctV=k7s&{7?B8GsbL|CdXy^TL)x=@&;lNxS5Pspf@_JA z-QhQv&}W2>_7e_StR6lGnb8HEXvrBxQ)^$k9}w_bS#%Cy`iMhU+R9zz!)U{FS3c-1 za)49~y+4j#k&=5X)>Xsl?ivVPR~v2aPTLF?Rti46^?=;?di6+>#?B-h#Qg8Gqmgg+ zgQv1-Mpjvs*!ZOD9gdg3R79;#dHYzpCkN6ZQQXxPCq6yBhF+#_^#{TVd6-5857+$Y z!GUb6!tRnnCZCm0>GJJpR?O0IFZ}*J*n5+QY^1{eES>;*z>zD_l&m!^l_jHdc8jmF zApxL#cP`>H=3(q^>ss=$MBO91L>UaJf8sL|;7i4Xzq*TC-vWAkci9E8x%;gsqhk{A&uNTp#?9)V@*@mYu{FkAa~=yj&0jj zs{fGavG1Dcv79Pb@muZyAX9y*1Me1lnUr(|O#T*elOJK%Q zZ?DMwgF*>r^@}{WCp)=+E%JCONQZ9Sk>+H)wDE1lpV0A6#S*<8#kIKB6e6d2_kt;= z7{)!ZeMl>dTPfWtRK~*F0b#7It>v;VO zX>_zOIyY#T@1|&;G+k#Zx~UUE`-Bk5;xH0;b8gBFLQ;6S;u3Qi?=q;lQ=BR?Imd-g zMami*zD4eSZg0wSAc4Oju3n12Byz}}QMFUy2t<}`vsWB>Wa{9BIY{&9FulQTh2LT6 z1-p8tw`46iE=rjYL60%GtDB2$o7nQwZn4N6=$b*jxP;L2P~#dQO|F-xl&-xcK3D}k zxlGNrd42F4$O$pkk(&t`Ug31`^ahEOtywB{rS?JiqdzDV-Np~E)Z;aIt_2Ru2kG7*qLS6DTs1| z7{1fUu)XK(a~!DR?@RBxrfAX$+LRMBG}r-%>GuX zor((L*Lo;xMn_z*sy9xG^wp>q35OA{bz+LByOg{?Q_F)FFV*(oUI>NKeGVd^P%Bu@ z7X5V;28LNPSm$+k>(Hy|8e>Rd+~!(AV2qpkNHo}}QbH$aKG;`-x_2np8pZhp=&PZvmwir?&i}QiyT0C0t$%R16q{XdTs{=YZt-4wr7z z^YG)P5|PKD1ob6@;!*-+M7x?vDY}OvRIdb%yaYaS%=(IrHQxecd-;+qF(@ufpA@Ou2krVdSO zuTX1iZEs}Ng|#X19VQt^u5MeDwBMc)Xg}<0icbztI+7aK(TA@$_{(ES%c~%ja4wBE zVzLN@R-V60N6U5$(8BuNCE!Pvwl9Ql9miDTn!RyCGC!sHAyB)I3dpe zgPb{ApAjvrLOH{1`dvoNu=zQ+7_y^xUdhIg>F#p1;lqf{C zym;Wya^$d*Xz&@r!s;i^)Vh!92Ugh!Ct)J{N!J&}-q)X<4>sE85C=vzTE&4D|4tFi z$8OY{*k9R=s3O6e2Z|=t(o+NvV;U>Bj zEPU*g>|Hj<*KmAr+G&PRyXNAL0t_ZRE;R{`hRP{~J@#H2scg%z4VB zXkHGo2VK4w^EA>n6Zru_6UyS1c0B;h$AM-Q_rSXJVka=xX$@+1Q~CbRhJ1FsAp=95?3 z*y*}F%I*fsj!9`KOzH?FkAFtH{b}k2){?op&DIX|t*;RWC(c{TqJQh-r+c`I%qOl> znwtfUf_gJ~OsR)PL6Q!6L;(+r4tW<~fZTy^68Exe)Ot^p#PTj3RvN~X4!`EF!ZuBE zy~b?R^|4vq=wob~8~69Te~Q?3^b z96qG{fHr|SxYPY_4+hyl(LF;keeO=wMIgw8`-W--VTRN40p+&@p*6diiV3MnwJQ;4 z5OGEh!p0VlvPg@u5TOl?Ps}7Wx8c9k#E%aFZ-Z`Z zlq{!Ba(M|Ws0d-;ZP1Kgt0M`Nw1$o~_WTfIvl|lN(udJlmbp*s*yNvn!9lvD+Z;4O zu`v1~+_1GU;lQeya4M8OXK!Q!;_Ea@v-ELb(8WCHYvZo10@~x%U#v4$I64#3g^jIEU04vFml> zx17)O+eZ1z?zfQ3Qe#$VI6@j}$7TCT7ObvHw7QJx!5 z09!IY@_GZt&rSP%--d@pi`Sk`+whhSiWw8Pz@!4G-Of>F9jsm?Pw9AE0jQG~MOB75 z#vAQE#0{)l0)KqsfrF+Wp$^3o>f;jwpZNjleAw30RlZp46?aC|DGWVA4jpshpWBA{ zxy|u$+Zf+H3`J4T)!e|`CS4V&QPTN_W_m$p`M(uDTOi{zZxw6vUK#`V5)?mI9rkVz zhmEJJ@@#0GT_!G4ld7q$sx@ML$AuY}uPC1{Rqvk&ifPwHc-qq&XcVp2MMVia#g-!4 ziUPeL$EokS&r32!TLKPL@b4J;?yk}^lr*Wi@=5oL(vIv9Zi=w=m}?*B8*Zu4>sk?m z2PJ^;PdqD#I&zj6nPO05M7BMcI;KNKmQS>`J>dloNwurEvDLU0KQGfKz^!{vfx+ss z#k!kR-2{hIDizs{X&=``?9h4FP2vKg?Nw*(%TfgmEAOY47?C)A&b8m7x)5%)dQCww zW{?Ifia5RQFK;iw1ne-_4L{KPIFXhzU6kDlUydBQ^iSUbe$958(U-=Ky+@Z)6~`l; z+1_~x4Nxp_Y@~V_SEy#<3Y&tHYXmkQ8Y+fob%zZNHfq6Cr}N=ycUBhEMAXj{Jtdl+ zQcts&xyD9UdLhErLw124m!wtQ{l9-ppdudbH^3k9RW7el#fb?R))KMTB%G=_*?Jdk z%5BXdSzFxF)FbuppmY91yd)iPl{RL~wTGIgr=fKy{?-i?yr?u7&y?3#`L|oIYO8s= z=AoKohqiV<2Fq)mFnRiVi|m?UjB+b{g2@kq)kqMLEhI5!+cIc4W~lz{T(GJE0L;D2 zpHK#pt4Mu386TIXCx<46&BZS;BA=5<6Y}Bqle-Y~1$7Ejl7USrwJdG_TB-{cZYx~-~kS^H9_QQ~A<&v$Z7k(-N^3;PSjBV^J z;Z+X^67B}`AnZj?9?a62ww2p9{6>TA@wj=*rjfQ1ylHd+v^re2ya|LAsrG#Q738oS z)Jn$>u@78BPip&@3!**bKA<2-UK9!?`$7QO6jYl`y*YRHc@{?e??;pB%3Y`~o-2na zB@W^i!)Y`KudpG7R5yLvT9mipKB(+KYCPcc{CNpAcY91E+PrvwLOz`k&Fp!P z2wWV^wyfH})RSwWMl{s3kOqglidC19spre6Gmiyp)nnhd+F5@|S4bfU5VgYrVDUxI ztm*cKGkzG4NtVGuf?_S7GShT{pC0E&J)Cc%75abIJg{FY8k|``ouc@sf)R<1^)$** zk_v`x4oCn{tzyC^IF zE9A7UY5mEIjG#@;YS6sC_%V32%<$XHq||jOqB> zP&Z2LIOD^Y5!CQxp5)rPBGm_49WWOZ1*H>uf9b5Jz}T%&JZ=Kk01RU6TqSlqTypSl zX>07Z+Ut56Gd3*%n*g9^IPoFJ#bJQg#a3pc!}e{I5SWStsTqOO=(PMcH?u9lLB zu9(9hZt?n4SEofAB6MwMLCf?NtB{GnEgWm)*O`I=25Je}98)jqhKM^JpId~b#|3GX z+jzg0?IG?X$((`m*)o<^Q&&pw2B-A*pU6o-HhdJew=O?S8Ne7F7ih|N_7Xyow;8?jBU+|3LB&3BfNdx3smKV)0ojvv*UG+t6Ip*sp@{eEU6C=%wBT+11c#()3Am4TNpZ zUI3{gnI}o?vGFa9fsf$yfsvRl*g$cPiX{CY5waXit#Wv4@Fws{JkG$OKPTF^bJ(SX zg4fMUF)icV8}{AtX)P*K0{{h**V`wmY)6S8hoT9iO7hf^uN>)SRf;?M<1h40b^q>& zyD01Q!n`(Gam+$`m0Q=(`n~L;B4f@iS&r2F&FI@Ui;Pb0xH6BV6#UY13CLBGIMx)M z?4BsY64Ay&S=d#KJALKB=~3;dM9@#_Mk=#g)7I?l)Ex@f#hID`Ak@bOhbyV}IIww3 zY;A%?CF~PeM|P#Z-Cb3GaUMbcdq%&;x)6AdGx>}A{6{JI{igGELE-cd?qpCoh^s$S zF-3jjJhi}ZmzYsSw5Xi=6g5CdMr9k%#j#=lX!^l)@6q{BYvl04^4*ey5>IKd1g#me z`=o6~G@nUvuk81+oO}We1>UsdqRjFXWqq46j}LEF1@n!Q*!VG<`-uiGU_%0`VXaPQqBoUle4%E!jfI%hFAHOC9-__X3Z zb(;5EW2DjxSy|05iaiut4_Gj&^O-xr+Gi0EJHh72wf~+mA2s=r8=7oA8_}1de&UfX zjuxM&T$aWlD)i2E7M+r5XudLRkZ(oe>r5DKD6p!@F!YaYXqEZm;yRX_n!xk6sOYn# zc7CS^=?!Bp?TEAao2Q?G*%-aM&{4#GR2C)~jP#T?~j4mReDeii|&=_^g zp#(oYV-+^TQ6bH^bN!2{C*)|lB>EBSLc}l-> zA8v+h+F9b3_`qDFX6-Gx9v#S7SHY+&7aStRm{>^Q9WrbD4Ug@^Gsutd{&3Xbhkl5@ z!``Y1_NTg}dp!h9f8udo^8%;aSmY|<31Bl2#*oy%7lbd_=8)kTX+&P6QY>{N0NY2V zowdMSmx>e_kf2G#+8W0X8087;k1esTPowh<)1K>QNLBqgKaU`)Z2Uh4h}KmZm4riK zKtgI6$MWiMsf0|Hz+TrbtKYd*8u13q4z9i|;}5w%VE45|He0=6dqD@k;AP`~m(65K z&Pqse^f0F!yORnt5xGp)#HblsHzqq*Tj`w&mn=?A6wd3Sl$gDy0^;vr=;gNwn{^$} zbfwF(C9DK+2~&!AzamqK8MJCfC=X+49Db}7(a$rasC&KA*ILPt2O9p@F_QNH(kEIo zN1#0IiH|$ep=Zd#ucv|e>%m5|v&ZRwttZ`F>A%OF{%6m5ioEaXK!_}J8O2j_S|ioG z72lH$Km2dFak~j&{BeRCU9H9VXNgC7CLE!-DNw-E*q{$}aE7+~8y%c`7tV{y93}aH z5OP5ZGB49gK@3$47OE_{i?x0?=rPlwBJcEkq)=;{GrdW~Fhocf@<;YbCKh^Dhhs7M zq#l8KZPb0%xnuW^KgKlRJOesMO9)j~c+M;{?Um$1dGKLW;G4m7ye-QwaFD_X8bMPG zhg=@=hOA>P_^m@bb}A@;Y16m`^o$xHcj|I@Cjb+jJuGPv(`k)Z_M$WQNbBKB*TSS3 z@Hh!A8CKLogXkWV6rH+8U2fjf8!>yt^ur`-!QHDc{r;S{ReFR<-$JJv2Ly`CKmzw9 zN0%n_1|EdBUJ3Pftf4%cLdm+U&Voa{UC+-73L?*sFN~JVw9hEiSKCPT-A84&;mZ2& zQKV1R9W|@F0_ZswSisB}>NM4xfBpq#G@1Jegd^Fsn1t0QBhG)I5AL4S4p%da?B{Ss z-)^+3cQ1`pJJ+voh1jtjX1pmf6jom6Ej^!*%FR8;4?+C>u~yh3v(oH{6mvhYtYA-! zI6LNm0pTXcG0QPdr_a30eFfjX*`r7o4yZy*)Z^kZ-+g1JD3izR_uS~?_&g!6F) zK$?o<8X{jWmMDTBBly+awmNv&cDiexL+>30Nz5~A6=goyUGjg2v^zmCmjxJwtQFO`Zy4zg8zqt3=WOePQt(VDt&j5Dn za3U~~`N$6v8|2!?ENxDTIB7V0n9qpSOUn&Qauq&V*CQ%=SDeXVr=n+EPWdvQs~Fyi zR=GLtK>pP~g#40Wb+<;@QU=Ql8;w3z(+kd0QGF?z)(sN_Y&8CeOjR8ESxP?(|Cvi- zU-)wZ#pW<$#S4#jd%qC@`Yhadg7JqRXT02^bu+s4>i!}{PKzB`$7KcR#mWjl?FR*k z=*3?X2Y4s{q_3Faj9V4IOLqt%c@vedF*!(3=A@LSDPGd*r7;QVod@k_d=Tpp_YJku zFFH?DH_=w3O%7ibe;q9~s=Qo7!6TCy7ETOsU>xFLjpJQP_IIx@EWA$VcK`NUIY}Xw zghbj{LtulfW2J~A4-x;~V<8{nFYZ)cTgDz!60EH<=|V2HeOXm}XxIlk44E!>VO&u^ z*}6QD2Xp4=vsnERtlgKQbz(&b<7n59%CkcNgEDZ68Ej7PMy0>XtV{XtsN|IRa37Yg zvWY7aZ4;UsjMrL(&Cz_(BImt+s)I(bm+d)CkrglfF z1#>>cNkz(KyI=}`bLhYOExLeb=~mCk7Q1G$bA=<(07HApd7<%Ke|iW&r!c<~)MLR0gCFt_$CdM5N#7O6|O*ud6etn_jm45&Mfk zGM1?n`_&i^^0~><_hLeiT|cmq6b*1M3VUuRVzwMfpvU+gVC;lE#`(JO&!-rS+Fe@5 z-NMxUhk&{3kADP=gN>b!nT?m3lTCx2SK!~n#>vRWA;88Ktf~|Ae*_$yENm>j|9^pd zq#faZ0@^+rI__$wUS!U$PL?+IKgis@oqv$oIJ=vJfdR7j?~veJwCL!@RK_Rd57odR zDG^!B5iuz-6_c?bG0CI?x%w?2DH}A%##a_b1||lG24Ken + + + + + #2b5797 + + + diff --git a/public/img/mstile-150x150.png b/public/img/mstile-150x150.png new file mode 100644 index 0000000000000000000000000000000000000000..2360303f2ad55e968b56f19dd18ab2d06f542120 GIT binary patch literal 9010 zcmdtIS2$eX7dJk7q6Uc)gGBE}@1!6^5WTlxL^pb8A|VMzg6Jg)L86V`8IcTP@YSL- z$%Ih`6B7nAc;|Qdzxdz27wtc-P{Mf&SZFvy@v}A4kNfh0>kryx9#t zadBJsGR8K}@1}0u(3i(^T)R8f8vD2V!~2&Xn^jzzRZSgx#0fn~B{Df)VoEtElyS}h z=}0pWJW=RVMi3zshn3yX|G!j3T)Cp?y?*-#+S{frdUpmRP9w*6sv0^8B7ZH>Z!(r( z&R_xDJH)U8j5d-+?`zlXUwuh6u6=8miHdLcq4%ekPR`Dn^@U1c5E^*=!oE+4t?24N zM|8x@ekVcdQ#InYaI5mHxf0U>wJjAv3D{1DZ80Vmb2SDnaYdDVYQ3)R_44P9hzr`%?F z;a|uKR_J@RG^+>Z?a>T#Pm#x6no!o8fQZzD$_Rt=XPuJ|UYkSj8$AzUMlNE^M${RZ ze7+~>TE;U1t(*z3z26~vZhx8Q!S?y{hFFZf_CA7?=U>mK9=^_vT1)&c8Z%zcG8X1L@vhI>VTJb%58b*TZqC5y zbORCR#Fux;xPXps;dSy){Fw_hc~bvCwiu?w7C;5!_PogoYitY*h*d>$lr+x*wO+KT z$_y}Ln66_FZe7Jut5WTT2rwkk;)tDk-h&--XN%n#+)A=6W3|k#uTT9EuJkw>YpSs1 z&|7FS(7ep0C3dx{b1d|W1-jr~k(*Qgm5mPq(JBj!85K}(QPe$-E!qoevOL~uMX=Tv z2&`IPg2(gXxiV&w@4uur5bFt%tMPS?i~CIBV1+KAcD%WRP3HGl_{=j5m=r$yDR4WB z)h%Z9%{~{}T%D+)?S=F}Y#^X_goY~*{tD>T9%D_lUc0_gyaV;*@DH-Ly#ZHyQo_{B zSo2cLoLA&kkniT3Pajn?RZBemZ_ex9BpN>w{Q@j~?r1Ah>;o2m;>_Gj%|igRa=1Wg z-GA}qIMViVJVs00ri1aQ4D>gw+^hL!|8zmf9eV%8V8BQ4xQ_aA^7tLC&2jbO*u}m; zW6!adlVOxGvYo2p?o8DSgV1IqelPl6i`W*p!1?

    9TcXe4aBrtVUSe8<)-#McI2T z3?hED}e(iS*W4bD=>JQ(0e{Ysja;1x;M*6=h;_}H#+#sx(aLD_ z-_17r4l#pcww0_p-+(2aD8nj77sz!7lc7W5@e>ie<(bq4`v%u?8+faJaf()cl~d&-%D(c#;HcJlqg1On z=wMF>k`Xr8Z&n7=`xd)7xg*wR6|l6u%!o|iJuyG7ecC1H&;EK&A89ye1p`BdufFLI z8+9h=cb*9S1eHyztpP311jR+tWGB;8DGRiOs*jJtQp3Z?9$ozQ=>(q)J;RjQ=!sP) zmiGIhKI1z_am}CR50^ec51V;6b-C@SacPSl>hil>fCQ$#xH(7u29kxTylAe-3!PA#&P=S_>T_r)f?}U3DWqEd1X}{eqz@nFvSEvV`rQ14<;HaUme$hO zkBye;n9E#zM_yG!RyB!?O$G(1 zSzl_&>krK*r4U9d#RVOEV^^QM`SySEI8{tZSCA9=LNjW>hHt7A54KEuJ<5%x`X)Q$~KZjk!P92-U3BjhGGdH93_)gTC zGoI~9r}?28{C({kR9s)*fmSFdn#@9_Sp6Y#dwZrAc6&Q*aS`VA+3F?hI;*W?Hg$^PL1t=d7obGE6>HLH1_!)4`VkxkKP5&ePM6_4M}1 zW?YOw(Pn>QI~KG*>^{PEraA`qUJe#MUf-Tbl5y3EIFp}MZb(4>9aBj385;A0lU=>< zj<>w{h_DbE6I}aKuznYB`;0U2-)h(iTJQwXzonssu!Obc&eLZJu+g$zDs2ij20Hhs z>=R_$mdaJm=?G)MOQ*BLBVO9IwfOM!5=j5M_XrEWtS2j4hpz|eQ@$byQ(?Mz=YJk> zcN!ANghg&yA*e2(lfuJ)|3?@c<;wYtpFK)foMwF3;vS`I&sQsQ zaXuNNkjEk0rCT$AoME^y_W!eT2R7K(XFMR?5azGf{^KcpSZDnj7q6hdnaf2K|M1c=^8EfAmm;HVk0T2Y0OX23jNY}`Rb8)A%qVUg`NjJ(M4P^ zq!2+VgI;L6jD=kUvJJuanz-K=_+nIAt`X|)8^=inO|HNs<(14A&iWRd9fp-HF~ z(9Mc4bcAtzSHJh|ES;tQ$|D9o3Af?k?d|QZr=uz3-7dVsbC7SMgL3q`(UMa_ff7XJ zp1#XPL6In;t{HnjDq#}k#sXuVAe=+jmDm$N94gbu#?>O6h@E}<% z!*pmZs*A4p3ZVkmc3u6+f0R$Cm6BH9Y|j7U@0*SwzdrM0jz_PMJ<{~93unnz%N24x zb8#OGQo~4Oe5*NCQQTCE8*Xpio>0ZqgCp%uTbrew%B@xPdGTQ1I@cxZo;%&S%$ zv+9m*wK(K!-i%thAa4IpMoPwYosfa-S;Ll@mFMGw~1eGxTw|)6Yrt(ZT6|@W~!Q93Q^#-WtFgQ)2ly84o4}R$v6c$*6^;{=rrRVJ>}t&Hld) zX4F>)9y>OAmg#vXuUNS^BJg5_Sw<;O+?5jv>|4>!O4&55XM9X*n58au^F+AdQb=?! zhL-EbQNGTr&l>Hs83`5po1Z5){+k<1C9kA%M?QIftduVRhG*9}9pj07NK%o$5r>Me zZYrt5eNugmxt(SRf#s8lz!`vnjJ;PXVsCv%ZG)xLV$n?eK60zB3B#!+wn0weSR_Bc zk4zsQhW4Xty^@S$wk491SH7X5**AUn>j_0)Pa4s5yNauc$Jt$*&P>{R!}0%;^>#uS z_VtcM%;nE6kQszR)$;yXj?FNZxc))L46B!1P7hm*Y$i7F+UP2ZlqD^ASf)lqVi-XM zOs0uen7U`){hNo9iY%Fcv{S^tHHJM`AObEYtC@68rK>2j+m49}ajqq8h=I85Wv;cR z52E@1xRa7nIL9AdNZ_B63Qd9(S}H(tRIXXx4)wBD4(beri#|(}uAysRm(|+AV-F_F z%8oXae#~E^V+}#hev|%!k@)8!ZJ$?*hhR?7ymqp+36jx{Sd0={T;#Il++VNF#HBZl( zt6uHn4fVOL*A>q>Zle^m9kD}a7rZ>jt9p4tro^y=Bs5@^$NYwrIXJR+@Yi-QJH6tY z@DqU(6{dk1XLy7I*IIuQ)5#<(ERW2Ev7&!KmAbgK0ctIw4q>-F9zd|Ql*2Enp6>9G ztC}1=E2Uu^iUcX@bb9gq+eKbt_754P&e|^~*eb){C|(Irthgn(2J4V73xq0N$t8Z$ zIaxc8d9CTa`hw8Eb1CQipOMXZX=WAx>$zh1KI*9ks2LdWUgnITwVpu?){HX#DJsOW z{uEh}V5dNLpsi*@$@)gar)^R)=jV)D+Yp%W}-RuAtzA9ssz$SQB#$VtzfOg_5mo#vY zJ&7cUp&c{g!en!D$zoTW;oJP_<@tLFE7?&!B*^|~xwn?>v=#pY_K4mvNd4f3J2%zb z{h+zw_XiRDOarU!J}WVC5B9{BP}Zmwku{!EI0vdirxV6prf%O+Do;UUR6u;5Dd{hEe;+_(VvU z=a$pPb1lddj$UM)InSp4-iApTU9zaT`GHj`3r{*HQ*`>`$|QV$eKMMk3xbM^Eh1}QN(Y9GIQ>i1Qb@EXWWpA>A{mbI}lCL4{|m%K>xR~E$mNKqe8-)iQ_YI1j6hHL=}r;1 zBRXCwK;NhGckm1fxjx%TNd~g?1CK}oP#T$&&J?Q;1rAxfqbom#NlB_oo8L=`l+}fd?WD?mO&Ws;4^BVDM1TwnvqmBI&_<>vy-qy_o_3!w^41s zu7qY6ZJYbO(*P?xAz7ExVHaXF07>HCXQ9rj3JHsGCGe;oSF-2LlL%f3;>SH4Vb*Ymh&M0gt14vhrX z-U_kBrg7zF5lc2#|GSVt9`UUDuKDOO9k-3o%WAg~x_7dpcyMH+Bj$|z!;kw7@#ktu zUgUQpirLh|JSdVr=>RmafD$xb2X0j;-#EjAhdfA%eY$%D@#l(Kb=RVeouX!uT}4CKFEmpC3_~%Bpp8ok1XJf;gcal2TRbh(P?#-CVJyk`&{*l$5RDaD|gb_$L6_Ie{ ztQ5zMv;i%Q$F%Bg1tj5yk`#t$CgIqh>WRqST}?*CQ#0%^*6Ax}T8fK5vdL`rtzT~K zXOaKzho?P8y06e|Ay27|FReGez}u+SETpZ4n^5tAQ;DR7i6YDVBNi{yOH)?2UflZqJxvgUw;t=wduXgpVhsj*uiR>0 zoXfW0l%0OKYf+jsQ9JD(`!FvXp_&E`OWZqQ3vshLeQR1gY!4$9K?E1l36g2y-bKv= z%d6&%h;R)SR_S38g+&y$Xy~*vcWTQ(m+90ZGIsaE<)Co;U;WjTn+DBvK7BoU7!jweV9C#PX|Aw;q~ zs0{U87YaW>Zt0=oPwFy?n)gs^;cNi^Mha$SdoWH*-PUI)X9MsQdc|NP@ z{6aVgTA7`{gnrn4;kBe#4^V@Pf+pbZ9f5r@gS8#;L2J*+9~VNy=J>{OCJQ6Fd~{N2 zvw>R34u#9O6>Ohc@tY(gD;uJzP1rqnqi2ai`*p*>gr}jMQ#z;hKZrqcBd>{=xd=k9 z{*_lV%t2n?-XWtaWf>SQo{@ZXMv2l5TPy6u(}VHr>S6`5ZgrXqsD5*62gj#cg9l-q zO~dDGHu=G@z|^|^q{c7I_#k+#gmo!>Wje1hS_91*HSp>8;Y2k?6FaI8^`u@p6twXv zUd;E{(rl{8WabY<8SY&4d;a9RFkKnwinE&xv#|>tZ)_P+4>*7F?kyUG9!f1mC(zKe z{E2!AG#oir4@YaWUL;od5kyYLx1D?ve#zc@sk(ed_YC#g0P^ za;b$3++W!ixwQD(=+bAPRV_cR#$?M@!W79q6l{uIa-abc4bjGM7j&SyIgNSua!c5H zi{!xva&{@jd`FX)bzg+=H$I-qydH&$p{dXe=q%j!r?872HSRxBf6Cp;+)@|&5yi=2 z$=phgAk@0&e&W)KiP|n#<#UR%;Pq~wm*z8^p!0cKvK>@OREbn%4)WywTFIm4zQgH; z5_v*Ly!YBa_jX#GH=+q)p>N0YBIx@rvMXdlv@|NOCcZL@B1Xqitxa0dUzUs^Lw_aS zCix9jG0blK?0FBA3+;e5#`_?O5X=1CEX!BRL>TF3pj~(P7cLNkh`UpsRiAoTB=01~ zL3DSG-86{VO>8weO%~_{CNhD~CZEd9J$HQ`|9X0=N**-~Z;jP1g8MMQ(`&weMO3GZ zi+tHaZL5k{CC=!WjhzQY4Mgu7xWpMWE{s>f}McfWWt6wu0+gT;qwF6vee zO-l(cfioV7o-36(b@LLv@lH{?mG9XF>0npo*nfOMMbrID;%PCJQ(vk~`AlLs5DXY; zI<4{_a#d4ikq+r*O%mNcD2@BI`4Ocd)J53$OtpOAt0V>*|co*Wc+er7eJvB$XTrE7P1sIgi;23gRcm4rNRUc^S$^ZF}1^pFgB6b@Z#opgdt-uPYM+<3E@ zKR>CPRr#5l4ymMJI283FH<@CP_M_w1icF|1EkT9F{r!&Ye|+G9`ndZmg>OGltPW!0 z0+koWjM&NP2q?06@%GD)Wt+!*DwfPFnOUVt?CVK_Oe}Y|1z>0^5em@4E_RuWK0485 zh3(@F&5CHU4@&!o`Ka*k3R86c9|tbI+czJLK?Ettr630m zfPVa*-h{VA^*H>&kE3j_ElAe5ZkrP){*gOHKo@*if>ur_rjh!v6Hav z5WI6S0~55K2*t#Jd#|5xqgbhqsoVf4uaR3siT2fb+s~u zr!;P5hcYSiM=73~C}v_V8}?59mjkNA6oM$~pg7_%FA$)#KHFFN^mo}nKwtImV|{m+ zh6+NLhjfkZt>*iiy&MJil@;58D=$;$CINoDkifS=xQBiK2=fprQD9+@0<4JahvrZH z&(!;TSB~v6Hhd$i$ScI#ty|cBHmoYT1Cb=2lemt{X%F{`E$*rgfVlgwA6s7%_=%4? z96Zj#Tl!B`SH^%G1s;YR_8EWO&}o^8Lc%_)_pQ<+rM?f$*Mggg1WQaFfnhI_M8_8AqrA&!G;$!*co%RT}fqCe+GBQGis zSrFK9_2_RCIANc12-#E#ws;s_g1tp?zBkX7PIF$<$Sz2sz0SCtehdmk4S~K+dxw2r5rJd;cJuK;(#ic1JM~+n75(eh@)$!c6r1F|z|&Dr@mc9}lXJ!IhF} z5MvBP!?_yPq);rbkj1I&rPe^U79FePm-@_>`V|fn5RBFggb8t%v|wmCD*IEaWE*g5 zymFdLmIyH##n~%G74eQK#7W{6@#_a`t6@Ih`68bxJZ1lXpuD|`xum*sZ{qO|X9JLO z;zKmtz&_l|BivilGt`?>0Te+B>arj;StXFQf|@4f1}RH}6g5Gh2ghKx{|`Z6u$M15 z=Kr5SwRWwALSPqbZ69vw5p^phG#Km~;C(ARCdB)eZ%DW&

    ' + cellHtml + ''; + if (rowClasses.length) { + rowClass = ' class="' + rowClasses.join(' ') + '"'; + } + + html += '' + cellHtml + ''; } return html; diff --git a/public/sass/components/_panel_table.scss b/public/sass/components/_panel_table.scss index fc14236c2b7..225238b102c 100644 --- a/public/sass/components/_panel_table.scss +++ b/public/sass/components/_panel_table.scss @@ -87,12 +87,6 @@ height: 100%; display: inline-block; } - - &.white { - a { - color: white; - } - } } &.cell-highlighted:hover { @@ -139,3 +133,17 @@ height: 0px; line-height: 0px; } + +.table-panel-color-cell { + color: white; + a { + color: white; + } +} + +.table-panel-color-row { + color: white; + a { + color: white; + } +} From 4962bf9d44ece9c08d28a25250d8f4125facb579 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Thu, 2 Aug 2018 15:49:50 +0200 Subject: [PATCH 259/786] remove info logging --- pkg/tsdb/sql_engine.go | 1 - 1 file changed, 1 deletion(-) diff --git a/pkg/tsdb/sql_engine.go b/pkg/tsdb/sql_engine.go index 29428971c64..3f681a5cdd7 100644 --- a/pkg/tsdb/sql_engine.go +++ b/pkg/tsdb/sql_engine.go @@ -253,7 +253,6 @@ func (e *sqlQueryEndpoint) transformToTimeSeries(query *Query, rows *core.Rows, columnType := columnTypes[i].DatabaseTypeName() for _, mct := range e.metricColumnTypes { - e.log.Info(mct) if columnType == mct { metricIndex = i continue From 7f4f130a803a80e15f2d34306904632b2ff142ed Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Thu, 2 Aug 2018 16:21:49 +0200 Subject: [PATCH 260/786] adjust test dashboards --- .../datasource_tests_mssql_unittest.json | 16 ++++++++-------- .../datasource_tests_mysql_unittest.json | 16 ++++++++-------- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/devenv/dev-dashboards/datasource_tests_mssql_unittest.json b/devenv/dev-dashboards/datasource_tests_mssql_unittest.json index 0c7cc0fcc65..80d3e1a5889 100644 --- a/devenv/dev-dashboards/datasource_tests_mssql_unittest.json +++ b/devenv/dev-dashboards/datasource_tests_mssql_unittest.json @@ -369,7 +369,7 @@ { "alias": "", "format": "time_series", - "rawSql": "SELECT $__timeGroup(time, '5m') AS time, avg(value) as value FROM metric WHERE $__timeFilter(time) GROUP BY $__timeGroup(time, '5m') ORDER BY 1", + "rawSql": "SELECT $__timeGroupAlias(time, '5m'), avg(value) as value FROM metric WHERE $__timeFilter(time) GROUP BY $__timeGroup(time, '5m') ORDER BY 1", "refId": "A" } ], @@ -452,7 +452,7 @@ { "alias": "", "format": "time_series", - "rawSql": "SELECT $__timeGroup(time, '5m', NULL) AS time, avg(value) as value FROM metric WHERE $__timeFilter(time) GROUP BY $__timeGroup(time, '5m') ORDER BY 1", + "rawSql": "SELECT $__timeGroupAlias(time, '5m', NULL), avg(value) as value FROM metric WHERE $__timeFilter(time) GROUP BY $__timeGroup(time, '5m') ORDER BY 1", "refId": "A" } ], @@ -535,7 +535,7 @@ { "alias": "", "format": "time_series", - "rawSql": "SELECT $__timeGroup(time, '5m', 10.0) AS time, avg(value) as value FROM metric WHERE $__timeFilter(time) GROUP BY $__timeGroup(time, '5m') ORDER BY 1", + "rawSql": "SELECT $__timeGroupAlias(time, '5m', 10.0), avg(value) as value FROM metric WHERE $__timeFilter(time) GROUP BY $__timeGroup(time, '5m') ORDER BY 1", "refId": "A" } ], @@ -618,7 +618,7 @@ { "alias": "", "format": "time_series", - "rawSql": "SELECT $__timeGroup(time, '$summarize') AS time, avg(value) as value FROM metric WHERE $__timeFilter(time) GROUP BY $__timeGroup(time, '$summarize') ORDER BY 1", + "rawSql": "SELECT $__timeGroupAlias(time, '$summarize'), avg(value) as value FROM metric WHERE $__timeFilter(time) GROUP BY $__timeGroup(time, '$summarize') ORDER BY 1", "refId": "A" } ], @@ -701,7 +701,7 @@ { "alias": "", "format": "time_series", - "rawSql": "SELECT $__timeGroup(time, '$summarize', NULL) AS time, sum(value) as value FROM metric WHERE $__timeFilter(time) GROUP BY $__timeGroup(time, '$summarize') ORDER BY 1", + "rawSql": "SELECT $__timeGroupAlias(time, '$summarize', NULL), sum(value) as value FROM metric WHERE $__timeFilter(time) GROUP BY $__timeGroup(time, '$summarize') ORDER BY 1", "refId": "A" } ], @@ -784,7 +784,7 @@ { "alias": "", "format": "time_series", - "rawSql": "SELECT $__timeGroup(time, '$summarize', 100.0) AS time, sum(value) as value FROM metric WHERE $__timeFilter(time) GROUP BY $__timeGroup(time, '$summarize') ORDER BY 1", + "rawSql": "SELECT $__timeGroupAlias(time, '$summarize', 100.0), sum(value) as value FROM metric WHERE $__timeFilter(time) GROUP BY $__timeGroup(time, '$summarize') ORDER BY 1", "refId": "A" } ], @@ -871,7 +871,7 @@ { "alias": "", "format": "time_series", - "rawSql": "SELECT \n $__timeGroup(time, '$summarize') as time, \n measurement as metric, \n avg(valueOne) as valueOne,\n avg(valueTwo) as valueTwo\nFROM\n metric_values \nWHERE\n $__timeFilter(time) AND\n ($metric = 'ALL' OR measurement = $metric)\nGROUP BY \n $__timeGroup(time, '$summarize'), \n measurement \nORDER BY 1", + "rawSql": "SELECT \n $__timeGroupAlias(time, '$summarize'), \n measurement as metric, \n avg(valueOne) as valueOne,\n avg(valueTwo) as valueTwo\nFROM\n metric_values \nWHERE\n $__timeFilter(time) AND\n ($metric = 'ALL' OR measurement = $metric)\nGROUP BY \n $__timeGroup(time, '$summarize'), \n measurement \nORDER BY 1", "refId": "A" } ], @@ -968,7 +968,7 @@ { "alias": "", "format": "time_series", - "rawSql": "SELECT \n $__timeGroup(time, '$summarize') as time, \n avg(valueOne) as valueOne, \n avg(valueTwo) as valueTwo \nFROM\n metric_values \nWHERE \n $__timeFilter(time) AND \n ($metric = 'ALL' OR measurement = $metric)\nGROUP BY \n $__timeGroup(time, '$summarize')\nORDER BY 1", + "rawSql": "SELECT \n $__timeGroupAlias(time, '$summarize'), \n avg(valueOne) as valueOne, \n avg(valueTwo) as valueTwo \nFROM\n metric_values \nWHERE \n $__timeFilter(time) AND \n ($metric = 'ALL' OR measurement = $metric)\nGROUP BY \n $__timeGroup(time, '$summarize')\nORDER BY 1", "refId": "A" }, { diff --git a/devenv/dev-dashboards/datasource_tests_mysql_unittest.json b/devenv/dev-dashboards/datasource_tests_mysql_unittest.json index e95eedf254c..f684186084a 100644 --- a/devenv/dev-dashboards/datasource_tests_mysql_unittest.json +++ b/devenv/dev-dashboards/datasource_tests_mysql_unittest.json @@ -369,7 +369,7 @@ { "alias": "", "format": "time_series", - "rawSql": "SELECT $__timeGroup(time, '5m') AS time, avg(value) as value FROM metric WHERE $__timeFilter(time) GROUP BY 1 ORDER BY 1", + "rawSql": "SELECT $__timeGroupAlias(time, '5m'), avg(value) as value FROM metric WHERE $__timeFilter(time) GROUP BY 1 ORDER BY 1", "refId": "A" } ], @@ -452,7 +452,7 @@ { "alias": "", "format": "time_series", - "rawSql": "SELECT $__timeGroup(time, '5m', NULL) AS time, avg(value) as value FROM metric WHERE $__timeFilter(time) GROUP BY 1 ORDER BY 1", + "rawSql": "SELECT $__timeGroupAlias(time, '5m', NULL), avg(value) as value FROM metric WHERE $__timeFilter(time) GROUP BY 1 ORDER BY 1", "refId": "A" } ], @@ -535,7 +535,7 @@ { "alias": "", "format": "time_series", - "rawSql": "SELECT $__timeGroup(time, '5m', 10.0) AS time, avg(value) as value FROM metric WHERE $__timeFilter(time) GROUP BY 1 ORDER BY 1", + "rawSql": "SELECT $__timeGroupAlias(time, '5m', 10.0), avg(value) as value FROM metric WHERE $__timeFilter(time) GROUP BY 1 ORDER BY 1", "refId": "A" } ], @@ -618,7 +618,7 @@ { "alias": "", "format": "time_series", - "rawSql": "SELECT $__timeGroup(time, '$summarize') AS time, avg(value) as value FROM metric WHERE $__timeFilter(time) GROUP BY 1 ORDER BY 1", + "rawSql": "SELECT $__timeGroupAlias(time, '$summarize'), avg(value) as value FROM metric WHERE $__timeFilter(time) GROUP BY 1 ORDER BY 1", "refId": "A" } ], @@ -701,7 +701,7 @@ { "alias": "", "format": "time_series", - "rawSql": "SELECT $__timeGroup(time, '$summarize', NULL) AS time, sum(value) as value FROM metric WHERE $__timeFilter(time) GROUP BY 1 ORDER BY 1", + "rawSql": "SELECT $__timeGroupAlias(time, '$summarize', NULL), sum(value) as value FROM metric WHERE $__timeFilter(time) GROUP BY 1 ORDER BY 1", "refId": "A" } ], @@ -784,7 +784,7 @@ { "alias": "", "format": "time_series", - "rawSql": "SELECT $__timeGroup(time, '$summarize', 100.0) AS time, sum(value) as value FROM metric WHERE $__timeFilter(time) GROUP BY 1 ORDER BY 1", + "rawSql": "SELECT $__timeGroupAlias(time, '$summarize', 100.0), sum(value) as value FROM metric WHERE $__timeFilter(time) GROUP BY 1 ORDER BY 1", "refId": "A" } ], @@ -871,7 +871,7 @@ { "alias": "", "format": "time_series", - "rawSql": "SELECT \n $__timeGroup(time, '$summarize') as time, \n measurement as metric, \n avg(valueOne) as valueOne,\n avg(valueTwo) as valueTwo\nFROM\n metric_values \nWHERE\n $__timeFilter(time) AND\n measurement IN($metric)\nGROUP BY 1, 2\nORDER BY 1", + "rawSql": "SELECT \n $__timeGroupAlias(time, '$summarize'), \n measurement as metric, \n avg(valueOne) as valueOne,\n avg(valueTwo) as valueTwo\nFROM\n metric_values \nWHERE\n $__timeFilter(time) AND\n measurement IN($metric)\nGROUP BY 1, 2\nORDER BY 1", "refId": "A" } ], @@ -968,7 +968,7 @@ { "alias": "", "format": "time_series", - "rawSql": "SELECT \n $__timeGroup(time, '$summarize') as time, \n avg(valueOne) as valueOne, \n avg(valueTwo) as valueTwo \nFROM\n metric_values \nWHERE \n $__timeFilter(time) AND \n measurement IN($metric)\nGROUP BY 1\nORDER BY 1", + "rawSql": "SELECT \n $__timeGroupAlias(time, '$summarize'), \n avg(valueOne) as valueOne, \n avg(valueTwo) as valueTwo \nFROM\n metric_values \nWHERE \n $__timeFilter(time) AND \n measurement IN($metric)\nGROUP BY 1\nORDER BY 1", "refId": "A" } ], From e5178b7d1d128a373ff33c304a3c26d0f5d77acc Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Thu, 2 Aug 2018 16:34:16 +0200 Subject: [PATCH 261/786] changelog: add notes about closing #12766 [skip ci] --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e6a116be927..6f7be5caae2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,6 +29,7 @@ * **Cloudwatch**: Added BurstBalance metric to list of AWS RDS metrics [#12561](https://github.com/grafana/grafana/pulls/12561), thx [@activeshadow](https://github.com/activeshadow) * **Cloudwatch**: Add new Redshift metrics and dimensions [#12063](https://github.com/grafana/grafana/pulls/12063), thx [@A21z](https://github.com/A21z) * **Table**: Adjust header contrast for the light theme [#12668](https://github.com/grafana/grafana/issues/12668) +* **Table**: Fix link color when using light theme and thresholds in use [#12766](https://github.com/grafana/grafana/issues/12766) * **Elasticsearch**: For alerting/backend, support having index name to the right of pattern in index pattern [#12731](https://github.com/grafana/grafana/issues/12731) * **OAuth**: Fix overriding tls_skip_verify_insecure using environment variable [#12747](https://github.com/grafana/grafana/issues/12747), thx [@jangaraj](https://github.com/jangaraj) * **Units**: Change units to include characters for power of 2 and 3 [#12744](https://github.com/grafana/grafana/pull/12744), thx [@Worty](https://github.com/Worty) From 7f85dd055ebc67f7c4855179fa819598170adb90 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Thu, 2 Aug 2018 16:44:29 +0200 Subject: [PATCH 262/786] changelog: add notes about closing #12749 [skip ci] --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6f7be5caae2..f699898e5f4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ * **Variables**: Skip unneeded extra query request when de-selecting variable values used for repeated panels [#8186](https://github.com/grafana/grafana/issues/8186), thx [@mtanda](https://github.com/mtanda) * **Postgres/MySQL/MSSQL**: Use floor rounding in $__timeGroup macro function [#12460](https://github.com/grafana/grafana/issues/12460), thx [@svenklemm](https://github.com/svenklemm) * **Postgres/MySQL/MSSQL**: Use metric column as prefix when returning multiple value columns [#12727](https://github.com/grafana/grafana/issues/12727), thx [@svenklemm](https://github.com/svenklemm) +* **Postgres/MySQL/MSSQL**: New $__timeGroupAlias macro. Postgres $__timeGroup no longer automatically adds time column alias [#12749](https://github.com/grafana/grafana/issues/12749), thx [@svenklemm](https://github.com/svenklemm) * **Postgres/MySQL/MSSQL**: Escape single quotes in variables [#12785](https://github.com/grafana/grafana/issues/12785), thx [@eMerzh](https://github.com/eMerzh) * **MySQL/MSSQL**: Use datetime format instead of epoch for $__timeFilter, $__timeFrom and $__timeTo macros [#11618](https://github.com/grafana/grafana/issues/11618) [#11619](https://github.com/grafana/grafana/issues/11619), thx [@AustinWinstanley](https://github.com/AustinWinstanley) * **Postgres**: Escape ssl mode parameter in connectionstring [#12644](https://github.com/grafana/grafana/issues/12644), thx [@yogyrahmawan](https://github.com/yogyrahmawan) @@ -36,6 +37,10 @@ * **Graph**: Option to hide series from tooltip [#3341](https://github.com/grafana/grafana/issues/3341), thx [@mtanda](https://github.com/mtanda) * **UI**: Fix iOS home screen "app" icon and Windows 10 app experience [#12752](https://github.com/grafana/grafana/issues/12752), thx [@andig](https://github.com/andig) +### Breaking changes + +* Postgres datasource no longer automatically adds time column alias when using the $__timeGroup alias. However, there's code in place which should make this change backward compatible and shouldn't create any issues. + # 5.2.2 (2018-07-25) ### Minor From cb76fc7f2d307faa9a530cc3cb66deee8aa31682 Mon Sep 17 00:00:00 2001 From: gzzo Date: Thu, 2 Aug 2018 12:29:47 -0400 Subject: [PATCH 263/786] Add auto_assign_org_id to defaults.ini For #12801 --- conf/defaults.ini | 3 +++ 1 file changed, 3 insertions(+) diff --git a/conf/defaults.ini b/conf/defaults.ini index 6c27886c649..b0caed81e90 100644 --- a/conf/defaults.ini +++ b/conf/defaults.ini @@ -213,6 +213,9 @@ allow_org_create = false # Set to true to automatically assign new users to the default organization (id 1) auto_assign_org = true +# Set this value to automatically add new users to the provided organization (if auto_assign_org above is set to true) +auto_assign_org_id = 1 + # Default role new users will be automatically assigned (if auto_assign_org above is set to true) auto_assign_org_role = Viewer From 72af8a70440761a470a9804fea5b367b0aff953c Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Thu, 2 Aug 2018 18:57:13 +0200 Subject: [PATCH 264/786] changelog: add notes about closing #1823 #12801 [skip ci] --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f699898e5f4..5298dcd04f6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ * **Cleanup**: Make temp file time to live configurable [#11607](https://github.com/grafana/grafana/issues/11607), thx [@xapon](https://github.com/xapon) * **LDAP**: Define Grafana Admin permission in ldap group mappings [#2469](https://github.com/grafana/grafana/issues/2496), PR [#12622](https://github.com/grafana/grafana/issues/12622) * **Cloudwatch**: CloudWatch GetMetricData support [#11487](https://github.com/grafana/grafana/issues/11487), thx [@mtanda](https://github.com/mtanda) +* **Configuration**: Allow auto-assigning users to specific organization (other than Main. Org) [#1823](https://github.com/grafana/grafana/issues/1823) [#12801](https://github.com/grafana/grafana/issues/12801), thx [@gzzo](https://github.com/gzzo) and [@ofosos](https://github.com/ofosos) ### Minor From 62d3655da43d712e32c1cb2f1a406c157e477478 Mon Sep 17 00:00:00 2001 From: Leonard Gram Date: Thu, 26 Jul 2018 13:40:52 +0200 Subject: [PATCH 265/786] docker: inital copy of the grafana-docker files. --- .circleci/config.yml | 28 ++++++++- packaging/docker/Dockerfile | 38 ++++++++++++ packaging/docker/build.sh | 22 +++++++ packaging/docker/push_to_docker_hub.sh | 17 ++++++ packaging/docker/run.sh | 82 ++++++++++++++++++++++++++ 5 files changed, 184 insertions(+), 3 deletions(-) create mode 100644 packaging/docker/Dockerfile create mode 100755 packaging/docker/build.sh create mode 100755 packaging/docker/push_to_docker_hub.sh create mode 100755 packaging/docker/run.sh diff --git a/.circleci/config.yml b/.circleci/config.yml index 44f34d42926..01cd36261fc 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -89,7 +89,7 @@ jobs: name: run linters command: 'gometalinter.v2 --enable-gc --vendor --deadline 10m --disable-all --enable=deadcode --enable=ineffassign --enable=structcheck --enable=unconvert --enable=varcheck ./...' - run: - name: run go vet + name: run go vet command: 'go vet ./pkg/...' test-frontend: @@ -159,6 +159,16 @@ jobs: - store_artifacts: path: dist + build-deploy-docker-master: + docker: + - image: docker:stable-git + steps: + - checkout + - setup_remote_docker + - run: docker info + - run: echo $GRAFANA_VERSION + - run: ./build.sh ${GRAFANA_VERSION} + build-enterprise: docker: - image: grafana/build-container:v0.1 @@ -246,7 +256,7 @@ workflows: test-and-build: jobs: - build-all: - filters: *filter-only-master + filters: *filter-not-release - build-enterprise: filters: *filter-only-master - codespell: @@ -270,7 +280,19 @@ workflows: - gometalinter - mysql-integration-test - postgres-integration-test - filters: *filter-only-master + filters: *filter-only-master + - build-deploy-docker-master: + requires: + - build-all + - test-backend + - test-frontend + - codespell + - gometalinter + - mysql-integration-test + - postgres-integration-test + filters: + branches: + only: grafana-docker - deploy-enterprise-master: requires: - build-all diff --git a/packaging/docker/Dockerfile b/packaging/docker/Dockerfile new file mode 100644 index 00000000000..6e4a5896b75 --- /dev/null +++ b/packaging/docker/Dockerfile @@ -0,0 +1,38 @@ +FROM debian:stretch-slim + +ARG GRAFANA_URL="https://s3-us-west-2.amazonaws.com/grafana-releases/master/grafana-latest.linux-x64.tar.gz" +ARG GF_UID="472" +ARG GF_GID="472" + +ENV PATH=/usr/share/grafana/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin \ + GF_PATHS_CONFIG="/etc/grafana/grafana.ini" \ + GF_PATHS_DATA="/var/lib/grafana" \ + GF_PATHS_HOME="/usr/share/grafana" \ + GF_PATHS_LOGS="/var/log/grafana" \ + GF_PATHS_PLUGINS="/var/lib/grafana/plugins" \ + GF_PATHS_PROVISIONING="/etc/grafana/provisioning" + +RUN apt-get update && apt-get install -qq -y tar libfontconfig curl ca-certificates && \ + mkdir -p "$GF_PATHS_HOME/.aws" && \ + curl "$GRAFANA_URL" | tar xfvz - --strip-components=1 -C "$GF_PATHS_HOME" && \ + apt-get autoremove -y && \ + rm -rf /var/lib/apt/lists/* && \ + groupadd -r -g $GF_GID grafana && \ + useradd -r -u $GF_UID -g grafana grafana && \ + mkdir -p "$GF_PATHS_PROVISIONING/datasources" \ + "$GF_PATHS_PROVISIONING/dashboards" \ + "$GF_PATHS_LOGS" \ + "$GF_PATHS_PLUGINS" \ + "$GF_PATHS_DATA" && \ + cp "$GF_PATHS_HOME/conf/sample.ini" "$GF_PATHS_CONFIG" && \ + cp "$GF_PATHS_HOME/conf/ldap.toml" /etc/grafana/ldap.toml && \ + chown -R grafana:grafana "$GF_PATHS_DATA" "$GF_PATHS_HOME/.aws" "$GF_PATHS_LOGS" "$GF_PATHS_PLUGINS" && \ + chmod 777 "$GF_PATHS_DATA" "$GF_PATHS_HOME/.aws" "$GF_PATHS_LOGS" "$GF_PATHS_PLUGINS" + +EXPOSE 3000 + +COPY ./run.sh /run.sh + +USER grafana +WORKDIR / +ENTRYPOINT [ "/run.sh" ] \ No newline at end of file diff --git a/packaging/docker/build.sh b/packaging/docker/build.sh new file mode 100755 index 00000000000..ac1dd41feec --- /dev/null +++ b/packaging/docker/build.sh @@ -0,0 +1,22 @@ +#!/bin/sh + +_grafana_tag=$1 +_grafana_version=$(echo ${_grafana_tag} | cut -d "v" -f 2) +_docker_repo=${2:-grafana/grafana} + + +echo ${_grafana_version} + +if [ "$_grafana_version" != "" ]; then + echo "Building version ${_grafana_version}" + docker build \ + --build-arg GRAFANA_URL="https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana-${_grafana_version}.linux-amd64.tar.gz" \ + --tag "${_docker_repo}:${_grafana_version}" \ + --no-cache=true . + docker tag ${_docker_repo}:${_grafana_version} ${_docker_repo}:latest +else + echo "Building latest for master" + docker build \ + --tag "grafana/grafana:master" \ + . +fi diff --git a/packaging/docker/push_to_docker_hub.sh b/packaging/docker/push_to_docker_hub.sh new file mode 100755 index 00000000000..4b23996f67f --- /dev/null +++ b/packaging/docker/push_to_docker_hub.sh @@ -0,0 +1,17 @@ +#!/bin/sh + +_grafana_tag=$1 +_grafana_version=$(echo ${_grafana_tag} | cut -d "v" -f 2) + +if [ "$_grafana_version" != "" ]; then + echo "pushing grafana/grafana:${_grafana_version}" + docker push grafana/grafana:${_grafana_version} + + if echo "$_grafana_version" | grep -viqF beta; then + echo "pushing grafana/grafana:latest" + docker push grafana/grafana:latest + fi +else + echo "pushing grafana/grafana:master" + docker push grafana/grafana:master +fi diff --git a/packaging/docker/run.sh b/packaging/docker/run.sh new file mode 100755 index 00000000000..44411f0f6b6 --- /dev/null +++ b/packaging/docker/run.sh @@ -0,0 +1,82 @@ +#!/bin/bash -e + +PERMISSIONS_OK=0 + +if [ ! -r "$GF_PATHS_CONFIG" ]; then + echo "GF_PATHS_CONFIG='$GF_PATHS_CONFIG' is not readable." + PERMISSIONS_OK=1 +fi + +if [ ! -w "$GF_PATHS_DATA" ]; then + echo "GF_PATHS_DATA='$GF_PATHS_DATA' is not writable." + PERMISSIONS_OK=1 +fi + +if [ ! -r "$GF_PATHS_HOME" ]; then + echo "GF_PATHS_HOME='$GF_PATHS_HOME' is not readable." + PERMISSIONS_OK=1 +fi + +if [ $PERMISSIONS_OK -eq 1 ]; then + echo "You may have issues with file permissions, more information here: http://docs.grafana.org/installation/docker/#migration-from-a-previous-version-of-the-docker-container-to-5-1-or-later" +fi + +if [ ! -d "$GF_PATHS_PLUGINS" ]; then + mkdir "$GF_PATHS_PLUGINS" +fi + +if [ ! -z ${GF_AWS_PROFILES+x} ]; then + > "$GF_PATHS_HOME/.aws/credentials" + + for profile in ${GF_AWS_PROFILES}; do + access_key_varname="GF_AWS_${profile}_ACCESS_KEY_ID" + secret_key_varname="GF_AWS_${profile}_SECRET_ACCESS_KEY" + region_varname="GF_AWS_${profile}_REGION" + + if [ ! -z "${!access_key_varname}" -a ! -z "${!secret_key_varname}" ]; then + echo "[${profile}]" >> "$GF_PATHS_HOME/.aws/credentials" + echo "aws_access_key_id = ${!access_key_varname}" >> "$GF_PATHS_HOME/.aws/credentials" + echo "aws_secret_access_key = ${!secret_key_varname}" >> "$GF_PATHS_HOME/.aws/credentials" + if [ ! -z "${!region_varname}" ]; then + echo "region = ${!region_varname}" >> "$GF_PATHS_HOME/.aws/credentials" + fi + fi + done + + chmod 600 "$GF_PATHS_HOME/.aws/credentials" +fi + +# Convert all environment variables with names ending in _FILE into the content of +# the file that they point at and use the name without the trailing _FILE. +# This can be used to carry in Docker secrets. +for VAR_NAME in $(env | grep '^GF_[^=]\+_FILE=.\+' | sed -r "s/([^=]*)_FILE=.*/\1/g"); do + VAR_NAME_FILE="$VAR_NAME"_FILE + if [ "${!VAR_NAME}" ]; then + echo >&2 "ERROR: Both $VAR_NAME and $VAR_NAME_FILE are set (but are exclusive)" + exit 1 + fi + echo "Getting secret $VAR_NAME from ${!VAR_NAME_FILE}" + export "$VAR_NAME"="$(< "${!VAR_NAME_FILE}")" + unset "$VAR_NAME_FILE" +done + +export HOME="$GF_PATHS_HOME" + +if [ ! -z "${GF_INSTALL_PLUGINS}" ]; then + OLDIFS=$IFS + IFS=',' + for plugin in ${GF_INSTALL_PLUGINS}; do + IFS=$OLDIFS + grafana-cli --pluginsDir "${GF_PATHS_PLUGINS}" plugins install ${plugin} + done +fi + +exec grafana-server \ + --homepath="$GF_PATHS_HOME" \ + --config="$GF_PATHS_CONFIG" \ + "$@" \ + cfg:default.log.mode="console" \ + cfg:default.paths.data="$GF_PATHS_DATA" \ + cfg:default.paths.logs="$GF_PATHS_LOGS" \ + cfg:default.paths.plugins="$GF_PATHS_PLUGINS" \ + cfg:default.paths.provisioning="$GF_PATHS_PROVISIONING" From bfe41d3cf15654f86e7c879b8e927f4daeaacff5 Mon Sep 17 00:00:00 2001 From: Leonard Gram Date: Thu, 26 Jul 2018 16:46:36 +0200 Subject: [PATCH 266/786] build: new workflow for PR:s and branches. --- .circleci/config.yml | 104 ++++++++++++++++++++++++++++------------- scripts/build/build.sh | 22 ++++----- 2 files changed, 81 insertions(+), 45 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 01cd36261fc..6dc3cdf378b 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -5,9 +5,11 @@ aliases: ignore: /.*/ tags: only: /^v[0-9]+(\.[0-9]+){2}(-.+|[^-.]*)$/ - - &filter-not-release + - &filter-not-release-or-master tags: ignore: /^v[0-9]+(\.[0-9]+){2}(-.+|[^-.]*)$/ + branches: + ignore: master - &filter-only-master branches: only: master @@ -156,18 +158,39 @@ jobs: - dist/grafana* - scripts/*.sh - scripts/publish - - store_artifacts: - path: dist - build-deploy-docker-master: - docker: - - image: docker:stable-git - steps: - - checkout - - setup_remote_docker - - run: docker info - - run: echo $GRAFANA_VERSION - - run: ./build.sh ${GRAFANA_VERSION} + build: + docker: + - image: grafana/build-container:1.0.0 + working_directory: /go/src/github.com/grafana/grafana + steps: + - checkout + - run: + name: prepare build tools + command: '/tmp/bootstrap.sh' + - run: + name: build and package grafana + command: './scripts/build/build.sh' + - run: + name: sign packages + command: './scripts/build/sign_packages.sh' + - run: + name: sha-sum packages + command: 'go run build.go sha-dist' + - persist_to_workspace: + root: . + paths: + - dist/grafana* + + build-docker: + docker: + - image: docker:stable-git + steps: + - checkout + - setup_remote_docker + - run: docker info + - run: echo $GRAFANA_VERSION + - run: cd packaging/docker && ./build.sh ${GRAFANA_VERSION} build-enterprise: docker: @@ -253,24 +276,24 @@ jobs: workflows: version: 2 - test-and-build: + build-master: jobs: - build-all: - filters: *filter-not-release + filters: *filter-only-master - build-enterprise: filters: *filter-only-master - codespell: - filters: *filter-not-release + filters: *filter-only-master - gometalinter: - filters: *filter-not-release + filters: *filter-only-master - test-frontend: - filters: *filter-not-release + filters: *filter-only-master - test-backend: - filters: *filter-not-release + filters: *filter-only-master - mysql-integration-test: - filters: *filter-not-release + filters: *filter-only-master - postgres-integration-test: - filters: *filter-not-release + filters: *filter-only-master - deploy-master: requires: - build-all @@ -281,18 +304,6 @@ workflows: - mysql-integration-test - postgres-integration-test filters: *filter-only-master - - build-deploy-docker-master: - requires: - - build-all - - test-backend - - test-frontend - - codespell - - gometalinter - - mysql-integration-test - - postgres-integration-test - filters: - branches: - only: grafana-docker - deploy-enterprise-master: requires: - build-all @@ -331,3 +342,32 @@ workflows: - mysql-integration-test - postgres-integration-test filters: *filter-only-release + + build-branches-and-prs: + jobs: + - build: + filters: *filter-not-release-or-master + - codespell: + filters: *filter-not-release-or-master + - gometalinter: + filters: *filter-not-release-or-master + - test-frontend: + filters: *filter-not-release-or-master + - test-backend: + filters: *filter-not-release-or-master + - mysql-integration-test: + filters: *filter-not-release-or-master + - postgres-integration-test: + filters: *filter-not-release-or-master + - build-docker: + requires: + - build + - test-backend + - test-frontend + - codespell + - gometalinter + - mysql-integration-test + - postgres-integration-test + filters: + branches: + only: grafana-docker diff --git a/scripts/build/build.sh b/scripts/build/build.sh index cee80822cac..a02f079dd72 100755 --- a/scripts/build/build.sh +++ b/scripts/build/build.sh @@ -14,12 +14,14 @@ echo "current dir: $(pwd)" if [ "$CIRCLE_TAG" != "" ]; then echo "Building releases from tag $CIRCLE_TAG" - CC=${CCX64} go run build.go -includeBuildNumber=false build + OPT="-includeBuildNumber=false" else echo "Building incremental build for $CIRCLE_BRANCH" - CC=${CCX64} go run build.go -buildNumber=${CIRCLE_BUILD_NUM} build + OPT="-buildNumber=${CIRCLE_BUILD_NUM}" fi +CC=${CCX64} go run build.go ${OPT} build + yarn install --pure-lockfile --no-progress echo "current dir: $(pwd)" @@ -28,14 +30,8 @@ if [ -d "dist" ]; then rm -rf dist fi -if [ "$CIRCLE_TAG" != "" ]; then - echo "Building frontend from tag $CIRCLE_TAG" - go run build.go -includeBuildNumber=false build-frontend - echo "Packaging a release from tag $CIRCLE_TAG" - go run build.go -goos linux -pkg-arch amd64 -includeBuildNumber=false package-only latest -else - echo "Building frontend for $CIRCLE_BRANCH" - go run build.go -buildNumber=${CIRCLE_BUILD_NUM} build-frontend - echo "Packaging incremental build for $CIRCLE_BRANCH" - go run build.go -goos linux -pkg-arch amd64 -buildNumber=${CIRCLE_BUILD_NUM} package-only latest -fi +echo "Building frontend" +go run build.go ${OPT} build-frontend + +echo "Packaging" +go run build.go -goos linux -pkg-arch amd64 ${OPT} package-only latest From e3a907214d822fd4db0f89cf1489165b742ec7ad Mon Sep 17 00:00:00 2001 From: Leonard Gram Date: Sat, 28 Jul 2018 23:00:59 +0200 Subject: [PATCH 267/786] build: builds docker image from local grafna tgz. --- .circleci/config.yml | 1 + packaging/docker/Dockerfile | 11 +++++++---- packaging/docker/build.sh | 1 - 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 6dc3cdf378b..74c4ee6c3ef 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -190,6 +190,7 @@ jobs: - setup_remote_docker - run: docker info - run: echo $GRAFANA_VERSION + - run: cp dist/grafana-latest.linux-x64.tar.gz packaging/docker - run: cd packaging/docker && ./build.sh ${GRAFANA_VERSION} build-enterprise: diff --git a/packaging/docker/Dockerfile b/packaging/docker/Dockerfile index 6e4a5896b75..3025b03f920 100644 --- a/packaging/docker/Dockerfile +++ b/packaging/docker/Dockerfile @@ -1,6 +1,6 @@ FROM debian:stretch-slim -ARG GRAFANA_URL="https://s3-us-west-2.amazonaws.com/grafana-releases/master/grafana-latest.linux-x64.tar.gz" +ARG GRAFANA_TGZ="grafana-latest.linux-x64.tar.gz" ARG GF_UID="472" ARG GF_GID="472" @@ -12,9 +12,12 @@ ENV PATH=/usr/share/grafana/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bi GF_PATHS_PLUGINS="/var/lib/grafana/plugins" \ GF_PATHS_PROVISIONING="/etc/grafana/provisioning" -RUN apt-get update && apt-get install -qq -y tar libfontconfig curl ca-certificates && \ +COPY ${GRAFANA_TGZ} /tmp/grafana.tar.gz + +RUN apt-get update && apt-get install -qq -y tar libfontconfig ca-certificates && \ mkdir -p "$GF_PATHS_HOME/.aws" && \ - curl "$GRAFANA_URL" | tar xfvz - --strip-components=1 -C "$GF_PATHS_HOME" && \ + tar xfvz /tmp/grafana.tar.gz --strip-components=1 -C "$GF_PATHS_HOME" && \ + rm /tmp/grafana.tar.gz && \ apt-get autoremove -y && \ rm -rf /var/lib/apt/lists/* && \ groupadd -r -g $GF_GID grafana && \ @@ -35,4 +38,4 @@ COPY ./run.sh /run.sh USER grafana WORKDIR / -ENTRYPOINT [ "/run.sh" ] \ No newline at end of file +ENTRYPOINT [ "/run.sh" ] diff --git a/packaging/docker/build.sh b/packaging/docker/build.sh index ac1dd41feec..df0a809c754 100755 --- a/packaging/docker/build.sh +++ b/packaging/docker/build.sh @@ -10,7 +10,6 @@ echo ${_grafana_version} if [ "$_grafana_version" != "" ]; then echo "Building version ${_grafana_version}" docker build \ - --build-arg GRAFANA_URL="https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana-${_grafana_version}.linux-amd64.tar.gz" \ --tag "${_docker_repo}:${_grafana_version}" \ --no-cache=true . docker tag ${_docker_repo}:${_grafana_version} ${_docker_repo}:latest From e8489304760d781498d49b31bbfb90515382c9f8 Mon Sep 17 00:00:00 2001 From: Leonard Gram Date: Sun, 29 Jul 2018 12:04:31 +0200 Subject: [PATCH 268/786] build: attach built resources. --- .circleci/config.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.circleci/config.yml b/.circleci/config.yml index 74c4ee6c3ef..5fe09fbb349 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -187,6 +187,8 @@ jobs: - image: docker:stable-git steps: - checkout + - attach_workspace: + at: . - setup_remote_docker - run: docker info - run: echo $GRAFANA_VERSION From 580e2c36d1575d205aad8b3c33f3d1b8b90a9b41 Mon Sep 17 00:00:00 2001 From: Leonard Gram Date: Mon, 30 Jul 2018 14:05:56 +0200 Subject: [PATCH 269/786] build: imported latest changes from grafana-docker. --- .circleci/config.yml | 7 +++-- packaging/docker/build-deploy.sh | 13 +++++++++ packaging/docker/build.sh | 37 +++++++++++++++----------- packaging/docker/deploy_to_k8s.sh | 6 +++++ packaging/docker/push_to_docker_hub.sh | 22 +++++++++------ packaging/docker/run.sh | 8 +++--- 6 files changed, 62 insertions(+), 31 deletions(-) create mode 100755 packaging/docker/build-deploy.sh create mode 100755 packaging/docker/deploy_to_k8s.sh diff --git a/.circleci/config.yml b/.circleci/config.yml index 5fe09fbb349..d59e4984454 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -182,7 +182,7 @@ jobs: paths: - dist/grafana* - build-docker: + grafana-docker-master: docker: - image: docker:stable-git steps: @@ -191,9 +191,8 @@ jobs: at: . - setup_remote_docker - run: docker info - - run: echo $GRAFANA_VERSION - run: cp dist/grafana-latest.linux-x64.tar.gz packaging/docker - - run: cd packaging/docker && ./build.sh ${GRAFANA_VERSION} + - run: cd packaging/docker && ./build-deploy.sh "grafana-docker-${CIRCLE_SHA1}" build-enterprise: docker: @@ -362,7 +361,7 @@ workflows: filters: *filter-not-release-or-master - postgres-integration-test: filters: *filter-not-release-or-master - - build-docker: + - grafana-docker-master: requires: - build - test-backend diff --git a/packaging/docker/build-deploy.sh b/packaging/docker/build-deploy.sh new file mode 100755 index 00000000000..923b1b8f3c0 --- /dev/null +++ b/packaging/docker/build-deploy.sh @@ -0,0 +1,13 @@ +#!/bin/sh + +_grafana_version=$1 +./build.sh "$_grafana_version" +docker login -u "$DOCKER_USER" -p "$DOCKER_PASS" + +#./push_to_docker_hub.sh "$_grafana_version" +echo "Would have deployed $_grafana_version" + +if echo "$_grafana_version" | grep -q "^master-"; then + apk add --no-cache curl + ./deploy_to_k8s.sh "grafana/grafana-dev:$_grafana_version" +fi diff --git a/packaging/docker/build.sh b/packaging/docker/build.sh index df0a809c754..579d65eebb3 100755 --- a/packaging/docker/build.sh +++ b/packaging/docker/build.sh @@ -1,21 +1,28 @@ #!/bin/sh _grafana_tag=$1 -_grafana_version=$(echo ${_grafana_tag} | cut -d "v" -f 2) -_docker_repo=${2:-grafana/grafana} - -echo ${_grafana_version} - -if [ "$_grafana_version" != "" ]; then - echo "Building version ${_grafana_version}" - docker build \ - --tag "${_docker_repo}:${_grafana_version}" \ - --no-cache=true . - docker tag ${_docker_repo}:${_grafana_version} ${_docker_repo}:latest +# If the tag starts with v, treat this as a official release +if echo "$_grafana_tag" | grep -q "^v"; then + _grafana_version=$(echo "${_grafana_tag}" | cut -d "v" -f 2) + _grafana_url="https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana-${_grafana_version}.linux-amd64.tar.gz" + _docker_repo=${2:-grafana/grafana} else - echo "Building latest for master" - docker build \ - --tag "grafana/grafana:master" \ - . + _grafana_version=$_grafana_tag + _grafana_url="https://s3-us-west-2.amazonaws.com/grafana-releases/master/grafana-${_grafana_version}.linux-x64.tar.gz" + _docker_repo=${2:-grafana/grafana-dev} +fi + +echo "Building ${_docker_repo}:${_grafana_version} from ${_grafana_url}" + +docker build \ + --build-arg GRAFANA_URL="${_grafana_url}" \ + --tag "${_docker_repo}:${_grafana_version}" \ + --no-cache=true . + +# Tag as 'latest' for official release; otherwise tag as grafana/grafana:master +if echo "$_grafana_tag" | grep -q "^v"; then + docker tag "${_docker_repo}:${_grafana_version}" "${_docker_repo}:latest" +else + docker tag "${_docker_repo}:${_grafana_version}" "grafana/grafana:master" fi diff --git a/packaging/docker/deploy_to_k8s.sh b/packaging/docker/deploy_to_k8s.sh new file mode 100755 index 00000000000..26cf88ef688 --- /dev/null +++ b/packaging/docker/deploy_to_k8s.sh @@ -0,0 +1,6 @@ +#!/bin/sh + +curl -s --header "Content-Type: application/json" \ + --data "{\"build_parameters\": {\"CIRCLE_JOB\": \"deploy\", \"IMAGE_NAMES\": \"$1\"}}" \ + --request POST \ + https://circleci.com/api/v1.1/project/github/raintank/deployment_tools/tree/master?circle-token=$CIRCLE_TOKEN diff --git a/packaging/docker/push_to_docker_hub.sh b/packaging/docker/push_to_docker_hub.sh index 4b23996f67f..e779b04d68d 100755 --- a/packaging/docker/push_to_docker_hub.sh +++ b/packaging/docker/push_to_docker_hub.sh @@ -1,16 +1,22 @@ #!/bin/sh _grafana_tag=$1 -_grafana_version=$(echo ${_grafana_tag} | cut -d "v" -f 2) -if [ "$_grafana_version" != "" ]; then - echo "pushing grafana/grafana:${_grafana_version}" - docker push grafana/grafana:${_grafana_version} +# If the tag starts with v, treat this as a official release +if echo "$_grafana_tag" | grep -q "^v"; then + _grafana_version=$(echo "${_grafana_tag}" | cut -d "v" -f 2) + _docker_repo=${2:-grafana/grafana} +else + _grafana_version=$_grafana_tag + _docker_repo=${2:-grafana/grafana-dev} +fi - if echo "$_grafana_version" | grep -viqF beta; then - echo "pushing grafana/grafana:latest" - docker push grafana/grafana:latest - fi +echo "pushing ${_docker_repo}:${_grafana_version}" +docker push "${_docker_repo}:${_grafana_version}" + +if echo "$_grafana_tag" | grep -q "^v"; then + echo "pushing ${_docker_repo}:latest" + docker push "${_docker_repo}:latest" else echo "pushing grafana/grafana:master" docker push grafana/grafana:master diff --git a/packaging/docker/run.sh b/packaging/docker/run.sh index 44411f0f6b6..2d2318a9210 100755 --- a/packaging/docker/run.sh +++ b/packaging/docker/run.sh @@ -46,11 +46,11 @@ if [ ! -z ${GF_AWS_PROFILES+x} ]; then chmod 600 "$GF_PATHS_HOME/.aws/credentials" fi -# Convert all environment variables with names ending in _FILE into the content of -# the file that they point at and use the name without the trailing _FILE. +# Convert all environment variables with names ending in __FILE into the content of +# the file that they point at and use the name without the trailing __FILE. # This can be used to carry in Docker secrets. -for VAR_NAME in $(env | grep '^GF_[^=]\+_FILE=.\+' | sed -r "s/([^=]*)_FILE=.*/\1/g"); do - VAR_NAME_FILE="$VAR_NAME"_FILE +for VAR_NAME in $(env | grep '^GF_[^=]\+__FILE=.\+' | sed -r "s/([^=]*)__FILE=.*/\1/g"); do + VAR_NAME_FILE="$VAR_NAME"__FILE if [ "${!VAR_NAME}" ]; then echo >&2 "ERROR: Both $VAR_NAME and $VAR_NAME_FILE are set (but are exclusive)" exit 1 From 424aa6e564fc6419c9192a4ee6cf74550f5aad67 Mon Sep 17 00:00:00 2001 From: Leonard Gram Date: Mon, 30 Jul 2018 16:35:30 +0200 Subject: [PATCH 270/786] build: removes unused args to docker build. --- packaging/docker/build.sh | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/packaging/docker/build.sh b/packaging/docker/build.sh index 579d65eebb3..c303c71cd5f 100755 --- a/packaging/docker/build.sh +++ b/packaging/docker/build.sh @@ -5,18 +5,15 @@ _grafana_tag=$1 # If the tag starts with v, treat this as a official release if echo "$_grafana_tag" | grep -q "^v"; then _grafana_version=$(echo "${_grafana_tag}" | cut -d "v" -f 2) - _grafana_url="https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana-${_grafana_version}.linux-amd64.tar.gz" _docker_repo=${2:-grafana/grafana} else _grafana_version=$_grafana_tag - _grafana_url="https://s3-us-west-2.amazonaws.com/grafana-releases/master/grafana-${_grafana_version}.linux-x64.tar.gz" _docker_repo=${2:-grafana/grafana-dev} fi -echo "Building ${_docker_repo}:${_grafana_version} from ${_grafana_url}" +echo "Building ${_docker_repo}:${_grafana_version}" docker build \ - --build-arg GRAFANA_URL="${_grafana_url}" \ --tag "${_docker_repo}:${_grafana_version}" \ --no-cache=true . From 99a9dbb04f161eac59cc7450c0daf5934a70129a Mon Sep 17 00:00:00 2001 From: Leonard Gram Date: Mon, 30 Jul 2018 18:52:49 +0200 Subject: [PATCH 271/786] build: complete docker build for master and releases. --- .circleci/config.yml | 46 +++++++++++++++++++++--------- packaging/docker/README.md | 45 +++++++++++++++++++++++++++++ packaging/docker/build-deploy.sh | 3 +- packaging/docker/custom/Dockerfile | 16 +++++++++++ 4 files changed, 95 insertions(+), 15 deletions(-) create mode 100644 packaging/docker/README.md create mode 100644 packaging/docker/custom/Dockerfile diff --git a/.circleci/config.yml b/.circleci/config.yml index d59e4984454..818f30f7eea 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -192,7 +192,19 @@ jobs: - setup_remote_docker - run: docker info - run: cp dist/grafana-latest.linux-x64.tar.gz packaging/docker - - run: cd packaging/docker && ./build-deploy.sh "grafana-docker-${CIRCLE_SHA1}" + - run: cd packaging/docker && ./build-deploy.sh "master-${CIRCLE_SHA1}" + + grafana-docker-release: + docker: + - image: docker:stable-git + steps: + - checkout + - attach_workspace: + at: . + - setup_remote_docker + - run: docker info + - run: cp dist/grafana-latest.linux-x64.tar.gz packaging/docker + - run: cd packaging/docker && ./build-deploy.sh "${CIRCLE_TAG}" build-enterprise: docker: @@ -306,6 +318,16 @@ workflows: - mysql-integration-test - postgres-integration-test filters: *filter-only-master + - grafana-docker-master: + requires: + - build-all + - test-backend + - test-frontend + - codespell + - gometalinter + - mysql-integration-test + - postgres-integration-test + filters: *filter-only-master - deploy-enterprise-master: requires: - build-all @@ -344,6 +366,16 @@ workflows: - mysql-integration-test - postgres-integration-test filters: *filter-only-release + - grafana-docker-release: + requires: + - build-all + - test-backend + - test-frontend + - codespell + - gometalinter + - mysql-integration-test + - postgres-integration-test + filters: *filter-only-release build-branches-and-prs: jobs: @@ -361,15 +393,3 @@ workflows: filters: *filter-not-release-or-master - postgres-integration-test: filters: *filter-not-release-or-master - - grafana-docker-master: - requires: - - build - - test-backend - - test-frontend - - codespell - - gometalinter - - mysql-integration-test - - postgres-integration-test - filters: - branches: - only: grafana-docker diff --git a/packaging/docker/README.md b/packaging/docker/README.md new file mode 100644 index 00000000000..d80cd87aebc --- /dev/null +++ b/packaging/docker/README.md @@ -0,0 +1,45 @@ +# Grafana Docker image + +[![CircleCI](https://circleci.com/gh/grafana/grafana-docker.svg?style=svg)](https://circleci.com/gh/grafana/grafana-docker) + +## Running your Grafana container + +Start your container binding the external port `3000`. + +```bash +docker run -d --name=grafana -p 3000:3000 grafana/grafana +``` + +Try it out, default admin user is admin/admin. + +## How to use the container + +Further documentation can be found at http://docs.grafana.org/installation/docker/ + +## Changelog + +### v5.1.5, v5.2.0-beta2 +* Fix: config keys ending with _FILE are not respected [#170](https://github.com/grafana/grafana-docker/issues/170) + +### v5.2.0-beta1 +* Support for Docker Secrets + +### v5.1.0 +* Major restructuring of the container +* Usage of `chown` removed +* File permissions incompatibility with previous versions + * user id changed from 104 to 472 + * group id changed from 107 to 472 +* Runs as the grafana user by default (instead of root) +* All default volumes removed + +### v4.2.0 +* Plugins are now installed into ${GF_PATHS_PLUGINS} +* Building the container now requires a full url to the deb package instead of just version +* Fixes bug caused by installing multiple plugins + +### v4.0.0-beta2 +* Plugins dir (`/var/lib/grafana/plugins`) is no longer a separate volume + +### v3.1.1 +* Make it possible to install specific plugin version https://github.com/grafana/grafana-docker/issues/59#issuecomment-260584026 \ No newline at end of file diff --git a/packaging/docker/build-deploy.sh b/packaging/docker/build-deploy.sh index 923b1b8f3c0..e20ae2c2a41 100755 --- a/packaging/docker/build-deploy.sh +++ b/packaging/docker/build-deploy.sh @@ -4,8 +4,7 @@ _grafana_version=$1 ./build.sh "$_grafana_version" docker login -u "$DOCKER_USER" -p "$DOCKER_PASS" -#./push_to_docker_hub.sh "$_grafana_version" -echo "Would have deployed $_grafana_version" +./push_to_docker_hub.sh "$_grafana_version" if echo "$_grafana_version" | grep -q "^master-"; then apk add --no-cache curl diff --git a/packaging/docker/custom/Dockerfile b/packaging/docker/custom/Dockerfile new file mode 100644 index 00000000000..79eba5f29e9 --- /dev/null +++ b/packaging/docker/custom/Dockerfile @@ -0,0 +1,16 @@ +ARG GRAFANA_VERSION="latest" + +FROM grafana/grafana:${GRAFANA_VERSION} + +USER grafana + +ARG GF_INSTALL_PLUGINS="" + +RUN if [ ! -z "${GF_INSTALL_PLUGINS}" ]; then \ + OLDIFS=$IFS; \ + IFS=','; \ + for plugin in ${GF_INSTALL_PLUGINS}; do \ + IFS=$OLDIFS; \ + grafana-cli --pluginsDir "$GF_PATHS_PLUGINS" plugins install ${plugin}; \ + done; \ +fi From b61ac546f157a0b00d49ce11f5d86f8345034a38 Mon Sep 17 00:00:00 2001 From: Leonard Gram Date: Mon, 30 Jul 2018 18:54:34 +0200 Subject: [PATCH 272/786] build: disables external docker build for master and release. --- .circleci/config.yml | 6 ------ 1 file changed, 6 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 818f30f7eea..e2deab62c1b 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -260,9 +260,6 @@ jobs: - run: name: Trigger Windows build command: './scripts/trigger_windows_build.sh ${APPVEYOR_TOKEN} ${CIRCLE_SHA1} master' - - run: - name: Trigger Docker build - command: './scripts/trigger_docker_build.sh ${TRIGGER_GRAFANA_PACKER_CIRCLECI_TOKEN} master-$(echo "${CIRCLE_SHA1}" | cut -b1-7)' - run: name: Publish to Grafana.com command: | @@ -284,9 +281,6 @@ jobs: - run: name: Trigger Windows build command: './scripts/trigger_windows_build.sh ${APPVEYOR_TOKEN} ${CIRCLE_SHA1} release' - - run: - name: Trigger Docker build - command: './scripts/trigger_docker_build.sh ${TRIGGER_GRAFANA_PACKER_CIRCLECI_TOKEN} ${CIRCLE_TAG}' workflows: version: 2 From bfc66a7ed0b395762eb72f4304569e4041711d8e Mon Sep 17 00:00:00 2001 From: Sven Klemm Date: Mon, 30 Jul 2018 11:04:04 +0200 Subject: [PATCH 273/786] add fillmode "last" to sql datasource This adds a new fill mode last (last observation carried forward) for grafana to the sql datasources. This fill mode will fill in the last seen value in a series when a timepoint is missing or NULL if no value for that series has been seen yet. --- docs/sources/features/datasources/mssql.md | 4 ++- docs/sources/features/datasources/mysql.md | 4 ++- docs/sources/features/datasources/postgres.md | 4 ++- pkg/tsdb/mssql/macros.go | 10 ++++-- pkg/tsdb/mssql/macros_test.go | 17 ++++++++-- pkg/tsdb/mysql/macros.go | 10 ++++-- pkg/tsdb/mysql/mysql_test.go | 31 ++++++++++++++++++- pkg/tsdb/postgres/macros.go | 10 ++++-- pkg/tsdb/postgres/postgres_test.go | 30 +++++++++++++++++- pkg/tsdb/sql_engine.go | 24 +++++++++++++- .../mssql/partials/query.editor.html | 4 ++- .../mysql/partials/query.editor.html | 4 ++- .../postgres/partials/query.editor.html | 4 ++- 13 files changed, 136 insertions(+), 20 deletions(-) diff --git a/docs/sources/features/datasources/mssql.md b/docs/sources/features/datasources/mssql.md index dabb896ec0f..524a93a943b 100644 --- a/docs/sources/features/datasources/mssql.md +++ b/docs/sources/features/datasources/mssql.md @@ -81,7 +81,9 @@ Macro example | Description *$__timeFrom()* | Will be replaced by the start of the currently active time selection. For example, *'2017-04-21T05:01:17Z'* *$__timeTo()* | Will be replaced by the end of the currently active time selection. For example, *'2017-04-21T05:06:17Z'* *$__timeGroup(dateColumn,'5m'[, fillvalue])* | Will be replaced by an expression usable in GROUP BY clause. Providing a *fillValue* of *NULL* or *floating value* will automatically fill empty series in timerange with that value.
    For example, *CAST(ROUND(DATEDIFF(second, '1970-01-01', time_column)/300.0, 0) as bigint)\*300*. -*$__timeGroup(dateColumn,'5m', 0)* | Same as above but with a fill parameter so all null values will be converted to the fill value (all null values would be set to zero using this example). +*$__timeGroup(dateColumn,'5m', 0)* | Same as above but with a fill parameter so missing points in that series will be added by grafana and 0 will be used as value. +*$__timeGroup(dateColumn,'5m', NULL)* | Same as above but NULL will be used as value for missing points. +*$__timeGroup(dateColumn,'5m', last)* | Same as above but the last seen value in that series will be used as fill value if no value has been seen yet NULL will be used. *$__timeGroupAlias(dateColumn,'5m')* | Will be replaced identical to $__timeGroup but with an added column alias (only available in Grafana 5.3+). *$__unixEpochFilter(dateColumn)* | Will be replaced by a time range filter using the specified column name with times represented as unix timestamp. For example, *dateColumn > 1494410783 AND dateColumn < 1494497183* *$__unixEpochFrom()* | Will be replaced by the start of the currently active time selection as unix timestamp. For example, *1494410783* diff --git a/docs/sources/features/datasources/mysql.md b/docs/sources/features/datasources/mysql.md index a0e67037005..153b3d7bbf5 100644 --- a/docs/sources/features/datasources/mysql.md +++ b/docs/sources/features/datasources/mysql.md @@ -64,7 +64,9 @@ Macro example | Description *$__timeFrom()* | Will be replaced by the start of the currently active time selection. For example, *'2017-04-21T05:01:17Z'* *$__timeTo()* | Will be replaced by the end of the currently active time selection. For example, *'2017-04-21T05:06:17Z'* *$__timeGroup(dateColumn,'5m')* | Will be replaced by an expression usable in GROUP BY clause. For example, *cast(cast(UNIX_TIMESTAMP(dateColumn)/(300) as signed)*300 as signed),* -*$__timeGroup(dateColumn,'5m',0)* | Same as above but with a fill parameter so all null values will be converted to the fill value (all null values would be set to zero using this example). +*$__timeGroup(dateColumn,'5m', 0)* | Same as above but with a fill parameter so missing points in that series will be added by grafana and 0 will be used as value. +*$__timeGroup(dateColumn,'5m', NULL)* | Same as above but NULL will be used as value for missing points. +*$__timeGroup(dateColumn,'5m', last)* | Same as above but the last seen value in that series will be used as fill value if no value has been seen yet NULL will be used. *$__timeGroupAlias(dateColumn,'5m')* | Will be replaced identical to $__timeGroup but with an added column alias (only available in Grafana 5.3+). *$__unixEpochFilter(dateColumn)* | Will be replaced by a time range filter using the specified column name with times represented as unix timestamp. For example, *dateColumn > 1494410783 AND dateColumn < 1494497183* *$__unixEpochFrom()* | Will be replaced by the start of the currently active time selection as unix timestamp. For example, *1494410783* diff --git a/docs/sources/features/datasources/postgres.md b/docs/sources/features/datasources/postgres.md index 35dfcac15c0..b776b7cbe58 100644 --- a/docs/sources/features/datasources/postgres.md +++ b/docs/sources/features/datasources/postgres.md @@ -61,7 +61,9 @@ Macro example | Description *$__timeFrom()* | Will be replaced by the start of the currently active time selection. For example, *'2017-04-21T05:01:17Z'* *$__timeTo()* | Will be replaced by the end of the currently active time selection. For example, *'2017-04-21T05:06:17Z'* *$__timeGroup(dateColumn,'5m')* | Will be replaced by an expression usable in GROUP BY clause. For example, *(extract(epoch from dateColumn)/300)::bigint*300* -*$__timeGroup(dateColumn,'5m', 0)* | Same as above but with a fill parameter so all null values will be converted to the fill value (all null values would be set to zero using this example). +*$__timeGroup(dateColumn,'5m', 0)* | Same as above but with a fill parameter so missing points in that series will be added by grafana and 0 will be used as value. +*$__timeGroup(dateColumn,'5m', NULL)* | Same as above but NULL will be used as value for missing points. +*$__timeGroup(dateColumn,'5m', last)* | Same as above but the last seen value in that series will be used as fill value if no value has been seen yet NULL will be used. *$__timeGroupAlias(dateColumn,'5m')* | Will be replaced identical to $__timeGroup but with an added column alias (only available in Grafana 5.3+). *$__unixEpochFilter(dateColumn)* | Will be replaced by a time range filter using the specified column name with times represented as unix timestamp. For example, *dateColumn >= 1494410783 AND dateColumn <= 1494497183* *$__unixEpochFrom()* | Will be replaced by the start of the currently active time selection as unix timestamp. For example, *1494410783* diff --git a/pkg/tsdb/mssql/macros.go b/pkg/tsdb/mssql/macros.go index f33ab1d40be..57a37d618e0 100644 --- a/pkg/tsdb/mssql/macros.go +++ b/pkg/tsdb/mssql/macros.go @@ -99,9 +99,13 @@ func (m *msSqlMacroEngine) evaluateMacro(name string, args []string) (string, er if len(args) == 3 { m.query.Model.Set("fill", true) m.query.Model.Set("fillInterval", interval.Seconds()) - if args[2] == "NULL" { - m.query.Model.Set("fillNull", true) - } else { + switch args[2] { + case "NULL": + m.query.Model.Set("fillMode", "null") + case "last": + m.query.Model.Set("fillMode", "last") + default: + m.query.Model.Set("fillMode", "value") floatVal, err := strconv.ParseFloat(args[2], 64) if err != nil { return "", fmt.Errorf("error parsing fill value %v", args[2]) diff --git a/pkg/tsdb/mssql/macros_test.go b/pkg/tsdb/mssql/macros_test.go index ea50c418de7..b808666d967 100644 --- a/pkg/tsdb/mssql/macros_test.go +++ b/pkg/tsdb/mssql/macros_test.go @@ -76,12 +76,25 @@ func TestMacroEngine(t *testing.T) { _, err := engine.Interpolate(query, timeRange, "GROUP BY $__timeGroup(time_column,'5m', NULL)") fill := query.Model.Get("fill").MustBool() - fillNull := query.Model.Get("fillNull").MustBool() + fillMode := query.Model.Get("fillMode").MustString() fillInterval := query.Model.Get("fillInterval").MustInt() So(err, ShouldBeNil) So(fill, ShouldBeTrue) - So(fillNull, ShouldBeTrue) + So(fillMode, ShouldEqual, "null") + So(fillInterval, ShouldEqual, 5*time.Minute.Seconds()) + }) + + Convey("interpolate __timeGroup function with fill (value = last)", func() { + _, err := engine.Interpolate(query, timeRange, "GROUP BY $__timeGroup(time_column,'5m', last)") + + fill := query.Model.Get("fill").MustBool() + fillMode := query.Model.Get("fillMode").MustString() + fillInterval := query.Model.Get("fillInterval").MustInt() + + So(err, ShouldBeNil) + So(fill, ShouldBeTrue) + So(fillMode, ShouldEqual, "last") So(fillInterval, ShouldEqual, 5*time.Minute.Seconds()) }) diff --git a/pkg/tsdb/mysql/macros.go b/pkg/tsdb/mysql/macros.go index a56fd1ceb2a..bebf4b396bb 100644 --- a/pkg/tsdb/mysql/macros.go +++ b/pkg/tsdb/mysql/macros.go @@ -94,9 +94,13 @@ func (m *mySqlMacroEngine) evaluateMacro(name string, args []string) (string, er if len(args) == 3 { m.query.Model.Set("fill", true) m.query.Model.Set("fillInterval", interval.Seconds()) - if args[2] == "NULL" { - m.query.Model.Set("fillNull", true) - } else { + switch args[2] { + case "NULL": + m.query.Model.Set("fillMode", "null") + case "last": + m.query.Model.Set("fillMode", "last") + default: + m.query.Model.Set("fillMode", "value") floatVal, err := strconv.ParseFloat(args[2], 64) if err != nil { return "", fmt.Errorf("error parsing fill value %v", args[2]) diff --git a/pkg/tsdb/mysql/mysql_test.go b/pkg/tsdb/mysql/mysql_test.go index 9947c23498b..fe262a3f758 100644 --- a/pkg/tsdb/mysql/mysql_test.go +++ b/pkg/tsdb/mysql/mysql_test.go @@ -295,7 +295,7 @@ func TestMySQL(t *testing.T) { }) - Convey("When doing a metric query using timeGroup with float fill enabled", func() { + Convey("When doing a metric query using timeGroup with value fill enabled", func() { query := &tsdb.TsdbQuery{ Queries: []*tsdb.Query{ { @@ -320,6 +320,35 @@ func TestMySQL(t *testing.T) { points := queryResult.Series[0].Points So(points[3][0].Float64, ShouldEqual, 1.5) }) + + Convey("When doing a metric query using timeGroup with last fill enabled", func() { + query := &tsdb.TsdbQuery{ + Queries: []*tsdb.Query{ + { + Model: simplejson.NewFromAny(map[string]interface{}{ + "rawSql": "SELECT $__timeGroup(time, '5m', last) as time_sec, avg(value) as value FROM metric GROUP BY 1 ORDER BY 1", + "format": "time_series", + }), + RefId: "A", + }, + }, + TimeRange: &tsdb.TimeRange{ + From: fmt.Sprintf("%v", fromStart.Unix()*1000), + To: fmt.Sprintf("%v", fromStart.Add(34*time.Minute).Unix()*1000), + }, + } + + resp, err := endpoint.Query(nil, nil, query) + So(err, ShouldBeNil) + queryResult := resp.Results["A"] + So(queryResult.Error, ShouldBeNil) + + points := queryResult.Series[0].Points + So(points[2][0].Float64, ShouldEqual, 15.0) + So(points[3][0].Float64, ShouldEqual, 15.0) + So(points[6][0].Float64, ShouldEqual, 20.0) + }) + }) Convey("Given a table with metrics having multiple values and measurements", func() { diff --git a/pkg/tsdb/postgres/macros.go b/pkg/tsdb/postgres/macros.go index 9e337caf3ec..3ab21ea0c6e 100644 --- a/pkg/tsdb/postgres/macros.go +++ b/pkg/tsdb/postgres/macros.go @@ -116,9 +116,13 @@ func (m *postgresMacroEngine) evaluateMacro(name string, args []string) (string, if len(args) == 3 { m.query.Model.Set("fill", true) m.query.Model.Set("fillInterval", interval.Seconds()) - if args[2] == "NULL" { - m.query.Model.Set("fillNull", true) - } else { + switch args[2] { + case "NULL": + m.query.Model.Set("fillMode", "null") + case "last": + m.query.Model.Set("fillMode", "last") + default: + m.query.Model.Set("fillMode", "value") floatVal, err := strconv.ParseFloat(args[2], 64) if err != nil { return "", fmt.Errorf("error parsing fill value %v", args[2]) diff --git a/pkg/tsdb/postgres/postgres_test.go b/pkg/tsdb/postgres/postgres_test.go index 3e864dca1e6..ac0964e912c 100644 --- a/pkg/tsdb/postgres/postgres_test.go +++ b/pkg/tsdb/postgres/postgres_test.go @@ -276,7 +276,7 @@ func TestPostgres(t *testing.T) { }) - Convey("When doing a metric query using timeGroup with float fill enabled", func() { + Convey("When doing a metric query using timeGroup with value fill enabled", func() { query := &tsdb.TsdbQuery{ Queries: []*tsdb.Query{ { @@ -303,6 +303,34 @@ func TestPostgres(t *testing.T) { }) }) + Convey("When doing a metric query using timeGroup with last fill enabled", func() { + query := &tsdb.TsdbQuery{ + Queries: []*tsdb.Query{ + { + Model: simplejson.NewFromAny(map[string]interface{}{ + "rawSql": "SELECT $__timeGroup(time, '5m', last), avg(value) as value FROM metric GROUP BY 1 ORDER BY 1", + "format": "time_series", + }), + RefId: "A", + }, + }, + TimeRange: &tsdb.TimeRange{ + From: fmt.Sprintf("%v", fromStart.Unix()*1000), + To: fmt.Sprintf("%v", fromStart.Add(34*time.Minute).Unix()*1000), + }, + } + + resp, err := endpoint.Query(nil, nil, query) + So(err, ShouldBeNil) + queryResult := resp.Results["A"] + So(queryResult.Error, ShouldBeNil) + + points := queryResult.Series[0].Points + So(points[2][0].Float64, ShouldEqual, 15.0) + So(points[3][0].Float64, ShouldEqual, 15.0) + So(points[6][0].Float64, ShouldEqual, 20.0) + }) + Convey("Given a table with metrics having multiple values and measurements", func() { type metric_values struct { Time time.Time diff --git a/pkg/tsdb/sql_engine.go b/pkg/tsdb/sql_engine.go index 3f681a5cdd7..f2f8b17db5f 100644 --- a/pkg/tsdb/sql_engine.go +++ b/pkg/tsdb/sql_engine.go @@ -274,9 +274,15 @@ func (e *sqlQueryEndpoint) transformToTimeSeries(query *Query, rows *core.Rows, fillMissing := query.Model.Get("fill").MustBool(false) var fillInterval float64 fillValue := null.Float{} + fillLast := false + if fillMissing { fillInterval = query.Model.Get("fillInterval").MustFloat64() * 1000 - if !query.Model.Get("fillNull").MustBool(false) { + switch query.Model.Get("fillMode").MustString() { + case "null": + case "last": + fillLast = true + case "value": fillValue.Float64 = query.Model.Get("fillValue").MustFloat64() fillValue.Valid = true } @@ -352,6 +358,14 @@ func (e *sqlQueryEndpoint) transformToTimeSeries(query *Query, rows *core.Rows, intervalStart = series.Points[len(series.Points)-1][1].Float64 + fillInterval } + if fillLast { + if len(series.Points) > 0 { + fillValue = series.Points[len(series.Points)-1][0] + } else { + fillValue.Valid = false + } + } + // align interval start intervalStart = math.Floor(intervalStart/fillInterval) * fillInterval @@ -377,6 +391,14 @@ func (e *sqlQueryEndpoint) transformToTimeSeries(query *Query, rows *core.Rows, intervalStart := series.Points[len(series.Points)-1][1].Float64 intervalEnd := float64(tsdbQuery.TimeRange.MustGetTo().UnixNano() / 1e6) + if fillLast { + if len(series.Points) > 0 { + fillValue = series.Points[len(series.Points)-1][0] + } else { + fillValue.Valid = false + } + } + // align interval start intervalStart = math.Floor(intervalStart/fillInterval) * fillInterval for i := intervalStart + fillInterval; i < intervalEnd; i += fillInterval { diff --git a/public/app/plugins/datasource/mssql/partials/query.editor.html b/public/app/plugins/datasource/mssql/partials/query.editor.html index e1320aabde2..e873d60ebbf 100644 --- a/public/app/plugins/datasource/mssql/partials/query.editor.html +++ b/public/app/plugins/datasource/mssql/partials/query.editor.html @@ -53,7 +53,9 @@ Macros: - $__timeEpoch(column) -> DATEDIFF(second, '1970-01-01', column) AS time - $__timeFilter(column) -> column BETWEEN '2017-04-21T05:01:17Z' AND '2017-04-21T05:01:17Z' - $__unixEpochFilter(column) -> column >= 1492750877 AND column <= 1492750877 -- $__timeGroup(column, '5m'[, fillvalue]) -> CAST(ROUND(DATEDIFF(second, '1970-01-01', column)/300.0, 0) as bigint)*300. Providing a fillValue of NULL or floating value will automatically fill empty series in timerange with that value. +- $__timeGroup(column, '5m'[, fillvalue]) -> CAST(ROUND(DATEDIFF(second, '1970-01-01', column)/300.0, 0) as bigint)*300. + by setting fillvalue grafana will fill in missing values according to the interval + fillvalue can be either a literal value, NULL or last; last will fill in the last seen value or NULL if none has been seen yet - $__timeGroupAlias(column, '5m'[, fillvalue]) -> CAST(ROUND(DATEDIFF(second, '1970-01-01', column)/300.0, 0) as bigint)*300 AS [time] Example of group by and order by with $__timeGroup: diff --git a/public/app/plugins/datasource/mysql/partials/query.editor.html b/public/app/plugins/datasource/mysql/partials/query.editor.html index db12a3fe8ce..664481ec8dc 100644 --- a/public/app/plugins/datasource/mysql/partials/query.editor.html +++ b/public/app/plugins/datasource/mysql/partials/query.editor.html @@ -53,7 +53,9 @@ Macros: - $__timeEpoch(column) -> UNIX_TIMESTAMP(column) as time_sec - $__timeFilter(column) -> column BETWEEN '2017-04-21T05:01:17Z' AND '2017-04-21T05:01:17Z' - $__unixEpochFilter(column) -> time_unix_epoch > 1492750877 AND time_unix_epoch < 1492750877 -- $__timeGroup(column,'5m') -> cast(cast(UNIX_TIMESTAMP(column)/(300) as signed)*300 as signed) +- $__timeGroup(column,'5m'[, fillvalue]) -> cast(cast(UNIX_TIMESTAMP(column)/(300) as signed)*300 as signed) + by setting fillvalue grafana will fill in missing values according to the interval + fillvalue can be either a literal value, NULL or last; last will fill in the last seen value or NULL if none has been seen yet - $__timeGroupAlias(column,'5m') -> cast(cast(UNIX_TIMESTAMP(column)/(300) as signed)*300 as signed) AS "time" Example of group by and order by with $__timeGroup: diff --git a/public/app/plugins/datasource/postgres/partials/query.editor.html b/public/app/plugins/datasource/postgres/partials/query.editor.html index 1b7278f6809..c455c0ebaf9 100644 --- a/public/app/plugins/datasource/postgres/partials/query.editor.html +++ b/public/app/plugins/datasource/postgres/partials/query.editor.html @@ -53,7 +53,9 @@ Macros: - $__timeEpoch -> extract(epoch from column) as "time" - $__timeFilter(column) -> column BETWEEN '2017-04-21T05:01:17Z' AND '2017-04-21T05:01:17Z' - $__unixEpochFilter(column) -> column >= 1492750877 AND column <= 1492750877 -- $__timeGroup(column,'5m') -> (extract(epoch from column)/300)::bigint*300 +- $__timeGroup(column,'5m'[, fillvalue]) -> (extract(epoch from column)/300)::bigint*300 + by setting fillvalue grafana will fill in missing values according to the interval + fillvalue can be either a literal value, NULL or last; last will fill in the last seen value or NULL if none has been seen yet - $__timeGroupAlias(column,'5m') -> (extract(epoch from column)/300)::bigint*300 AS "time" Example of group by and order by with $__timeGroup: From 83d7ec1da2b9a00a542e955f6a41d4a6dbf75c63 Mon Sep 17 00:00:00 2001 From: Sven Klemm Date: Tue, 31 Jul 2018 06:36:45 +0200 Subject: [PATCH 274/786] specify grafana version for last fill mode --- docs/sources/features/datasources/mssql.md | 2 +- docs/sources/features/datasources/mysql.md | 2 +- docs/sources/features/datasources/postgres.md | 1 + 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/sources/features/datasources/mssql.md b/docs/sources/features/datasources/mssql.md index 524a93a943b..9a149df120d 100644 --- a/docs/sources/features/datasources/mssql.md +++ b/docs/sources/features/datasources/mssql.md @@ -83,7 +83,7 @@ Macro example | Description *$__timeGroup(dateColumn,'5m'[, fillvalue])* | Will be replaced by an expression usable in GROUP BY clause. Providing a *fillValue* of *NULL* or *floating value* will automatically fill empty series in timerange with that value.
    For example, *CAST(ROUND(DATEDIFF(second, '1970-01-01', time_column)/300.0, 0) as bigint)\*300*. *$__timeGroup(dateColumn,'5m', 0)* | Same as above but with a fill parameter so missing points in that series will be added by grafana and 0 will be used as value. *$__timeGroup(dateColumn,'5m', NULL)* | Same as above but NULL will be used as value for missing points. -*$__timeGroup(dateColumn,'5m', last)* | Same as above but the last seen value in that series will be used as fill value if no value has been seen yet NULL will be used. +*$__timeGroup(dateColumn,'5m', last)* | Same as above but the last seen value in that series will be used as fill value if no value has been seen yet NULL will be used (only available in Grafana 5.3+). *$__timeGroupAlias(dateColumn,'5m')* | Will be replaced identical to $__timeGroup but with an added column alias (only available in Grafana 5.3+). *$__unixEpochFilter(dateColumn)* | Will be replaced by a time range filter using the specified column name with times represented as unix timestamp. For example, *dateColumn > 1494410783 AND dateColumn < 1494497183* *$__unixEpochFrom()* | Will be replaced by the start of the currently active time selection as unix timestamp. For example, *1494410783* diff --git a/docs/sources/features/datasources/mysql.md b/docs/sources/features/datasources/mysql.md index 153b3d7bbf5..4f4efb6e29a 100644 --- a/docs/sources/features/datasources/mysql.md +++ b/docs/sources/features/datasources/mysql.md @@ -66,7 +66,7 @@ Macro example | Description *$__timeGroup(dateColumn,'5m')* | Will be replaced by an expression usable in GROUP BY clause. For example, *cast(cast(UNIX_TIMESTAMP(dateColumn)/(300) as signed)*300 as signed),* *$__timeGroup(dateColumn,'5m', 0)* | Same as above but with a fill parameter so missing points in that series will be added by grafana and 0 will be used as value. *$__timeGroup(dateColumn,'5m', NULL)* | Same as above but NULL will be used as value for missing points. -*$__timeGroup(dateColumn,'5m', last)* | Same as above but the last seen value in that series will be used as fill value if no value has been seen yet NULL will be used. +*$__timeGroup(dateColumn,'5m', last)* | Same as above but the last seen value in that series will be used as fill value if no value has been seen yet NULL will be used (only available in Grafana 5.3+). *$__timeGroupAlias(dateColumn,'5m')* | Will be replaced identical to $__timeGroup but with an added column alias (only available in Grafana 5.3+). *$__unixEpochFilter(dateColumn)* | Will be replaced by a time range filter using the specified column name with times represented as unix timestamp. For example, *dateColumn > 1494410783 AND dateColumn < 1494497183* *$__unixEpochFrom()* | Will be replaced by the start of the currently active time selection as unix timestamp. For example, *1494410783* diff --git a/docs/sources/features/datasources/postgres.md b/docs/sources/features/datasources/postgres.md index b776b7cbe58..f2b54d3f0ce 100644 --- a/docs/sources/features/datasources/postgres.md +++ b/docs/sources/features/datasources/postgres.md @@ -64,6 +64,7 @@ Macro example | Description *$__timeGroup(dateColumn,'5m', 0)* | Same as above but with a fill parameter so missing points in that series will be added by grafana and 0 will be used as value. *$__timeGroup(dateColumn,'5m', NULL)* | Same as above but NULL will be used as value for missing points. *$__timeGroup(dateColumn,'5m', last)* | Same as above but the last seen value in that series will be used as fill value if no value has been seen yet NULL will be used. +*$__timeGroup(dateColumn,'5m', last)* | Same as above but the last seen value in that series will be used as fill value if no value has been seen yet NULL will be used (only available in Grafana 5.3+). *$__timeGroupAlias(dateColumn,'5m')* | Will be replaced identical to $__timeGroup but with an added column alias (only available in Grafana 5.3+). *$__unixEpochFilter(dateColumn)* | Will be replaced by a time range filter using the specified column name with times represented as unix timestamp. For example, *dateColumn >= 1494410783 AND dateColumn <= 1494497183* *$__unixEpochFrom()* | Will be replaced by the start of the currently active time selection as unix timestamp. For example, *1494410783* From 2cae966e6ccfb50e6eb7432ed425a06eed600ed9 Mon Sep 17 00:00:00 2001 From: Sven Klemm Date: Thu, 2 Aug 2018 21:40:15 +0200 Subject: [PATCH 275/786] use $__timeGroupAlias macro --- .../datasource/postgres/postgres_query.ts | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/public/app/plugins/datasource/postgres/postgres_query.ts b/public/app/plugins/datasource/postgres/postgres_query.ts index 9715665fd4b..fb6fa59ada1 100644 --- a/public/app/plugins/datasource/postgres/postgres_query.ts +++ b/public/app/plugins/datasource/postgres/postgres_query.ts @@ -92,7 +92,7 @@ export default class PostgresQuery { } } - buildTimeColumn() { + buildTimeColumn(alias = true) { let timeGroup = this.hasTimeGroup(); let query; @@ -103,9 +103,16 @@ export default class PostgresQuery { } else { args = timeGroup.params[0]; } - query = '$__timeGroup(' + this.target.timeColumn + ',' + args + ')'; + if (alias) { + query = '$__timeGroupAlias(' + this.target.timeColumn + ',' + args + ')'; + } else { + query = '$__timeGroup(' + this.target.timeColumn + ',' + args + ')'; + } } else { - query = this.target.timeColumn + ' AS "time"'; + query = this.target.timeColumn; + if (alias) { + query += ' AS "time"'; + } } return query; @@ -162,9 +169,7 @@ export default class PostgresQuery { if (this.hasMetricColumn()) { overParts.push('PARTITION BY ' + this.target.metricColumn); } - if (!aggregate) { - overParts.push('ORDER BY ' + this.target.timeColumn); - } + overParts.push('ORDER BY ' + this.buildTimeColumn(false)); let over = overParts.join(' '); let curr: string; From 0b57e88f9e025b6bf7f35472a1be845f2bb140c9 Mon Sep 17 00:00:00 2001 From: Sven Klemm Date: Fri, 3 Aug 2018 06:19:53 +0200 Subject: [PATCH 276/786] adjust frontend test --- .../datasource/postgres/specs/postgres_query.jest.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/public/app/plugins/datasource/postgres/specs/postgres_query.jest.ts b/public/app/plugins/datasource/postgres/specs/postgres_query.jest.ts index c589eb3c43c..c1d5400d1fa 100644 --- a/public/app/plugins/datasource/postgres/specs/postgres_query.jest.ts +++ b/public/app/plugins/datasource/postgres/specs/postgres_query.jest.ts @@ -30,10 +30,11 @@ describe('PostgresQuery', function() { { timeColumn: 'time', group: [{ type: 'time', params: ['5m', 'none'] }] }, templateSrv ); - expect(query.buildTimeColumn()).toBe('$__timeGroup(time,5m)'); + expect(query.buildTimeColumn()).toBe('$__timeGroupAlias(time,5m)'); + expect(query.buildTimeColumn(false)).toBe('$__timeGroup(time,5m)'); query = new PostgresQuery({ timeColumn: 'time', group: [{ type: 'time', params: ['5m', 'NULL'] }] }, templateSrv); - expect(query.buildTimeColumn()).toBe('$__timeGroup(time,5m,NULL)'); + expect(query.buildTimeColumn()).toBe('$__timeGroupAlias(time,5m,NULL)'); }); describe('When generating metric column SQL', function() { @@ -97,8 +98,8 @@ describe('PostgresQuery', function() { { type: 'window', params: ['increase'] }, ]; expect(query.buildValueColumn(column)).toBe( - '(CASE WHEN max(v ORDER BY time) >= lag(max(v ORDER BY time)) OVER (PARTITION BY host) ' + - 'THEN max(v ORDER BY time) - lag(max(v ORDER BY time)) OVER (PARTITION BY host) ELSE max(v ORDER BY time) END) AS "a"' + '(CASE WHEN max(v ORDER BY time) >= lag(max(v ORDER BY time)) OVER (PARTITION BY host ORDER BY time) ' + + 'THEN max(v ORDER BY time) - lag(max(v ORDER BY time)) OVER (PARTITION BY host ORDER BY time) ELSE max(v ORDER BY time) END) AS "a"' ); }); From dabfd88cd9eaadf55467f34118a37a021bf0b63b Mon Sep 17 00:00:00 2001 From: Sven Klemm Date: Fri, 3 Aug 2018 07:44:36 +0200 Subject: [PATCH 277/786] add moving average to query builder --- .../datasource/postgres/postgres_query.ts | 43 +++++++++++-------- .../plugins/datasource/postgres/query_ctrl.ts | 17 ++++++-- .../plugins/datasource/postgres/sql_part.ts | 19 ++++++++ 3 files changed, 58 insertions(+), 21 deletions(-) diff --git a/public/app/plugins/datasource/postgres/postgres_query.ts b/public/app/plugins/datasource/postgres/postgres_query.ts index fb6fa59ada1..d5bdf005e8f 100644 --- a/public/app/plugins/datasource/postgres/postgres_query.ts +++ b/public/app/plugins/datasource/postgres/postgres_query.ts @@ -142,7 +142,7 @@ export default class PostgresQuery { query = columnName.params[0]; let aggregate = _.find(column, (g: any) => g.type === 'aggregate' || g.type === 'percentile'); - let windows = _.find(column, (g: any) => g.type === 'window'); + let windows = _.find(column, (g: any) => g.type === 'window' || g.type === 'moving_window'); if (aggregate) { let func = aggregate.params[0]; @@ -174,25 +174,32 @@ export default class PostgresQuery { let over = overParts.join(' '); let curr: string; let prev: string; - switch (windows.params[0]) { - case 'increase': - curr = query; - prev = 'lag(' + curr + ') OVER (' + over + ')'; - query = '(CASE WHEN ' + curr + ' >= ' + prev + ' THEN ' + curr + ' - ' + prev + ' ELSE ' + curr + ' END)'; - break; - case 'rate': - let timeColumn = this.target.timeColumn; - if (aggregate) { - timeColumn = 'min(' + timeColumn + ')'; - } + switch (windows.type) { + case 'window': + switch (windows.params[0]) { + case 'increase': + curr = query; + prev = 'lag(' + curr + ') OVER (' + over + ')'; + query = '(CASE WHEN ' + curr + ' >= ' + prev + ' THEN ' + curr + ' - ' + prev + ' ELSE ' + curr + ' END)'; + break; + case 'rate': + let timeColumn = this.target.timeColumn; + if (aggregate) { + timeColumn = 'min(' + timeColumn + ')'; + } - curr = query; - prev = 'lag(' + curr + ') OVER (' + over + ')'; - query = '(CASE WHEN ' + curr + ' >= ' + prev + ' THEN ' + curr + ' - ' + prev + ' ELSE ' + curr + ' END)'; - query += '/extract(epoch from ' + timeColumn + ' - lag(' + timeColumn + ') OVER (' + over + '))'; + curr = query; + prev = 'lag(' + curr + ') OVER (' + over + ')'; + query = '(CASE WHEN ' + curr + ' >= ' + prev + ' THEN ' + curr + ' - ' + prev + ' ELSE ' + curr + ' END)'; + query += '/extract(epoch from ' + timeColumn + ' - lag(' + timeColumn + ') OVER (' + over + '))'; + break; + default: + query = windows.params[0] + '(' + query + ') OVER (' + over + ')'; + break; + } break; - default: - query = windows.params[0] + '(' + query + ') OVER (' + over + ')'; + case 'moving_window': + query = windows.params[0] + '(' + query + ') OVER (' + over + ' ROWS ' + windows.params[1] + ' PRECEDING)'; break; } } diff --git a/public/app/plugins/datasource/postgres/query_ctrl.ts b/public/app/plugins/datasource/postgres/query_ctrl.ts index a5867cd09f6..6138896d42a 100644 --- a/public/app/plugins/datasource/postgres/query_ctrl.ts +++ b/public/app/plugins/datasource/postgres/query_ctrl.ts @@ -138,6 +138,7 @@ export class PostgresQueryCtrl extends QueryCtrl { { text: 'Increase', value: 'increase' }, { text: 'Rate', value: 'rate' }, { text: 'Sum', value: 'sum' }, + { text: 'Moving Average', value: 'avg', type: 'moving_window' }, ], }; this.selectMenu.push(windows); @@ -263,14 +264,22 @@ export class PostgresQueryCtrl extends QueryCtrl { return _.findIndex(selectParts, (p: any) => p.def.type === 'aggregate' || p.def.type === 'percentile'); } + findWindowIndex(selectParts) { + return _.findIndex(selectParts, (p: any) => p.def.type === 'window' || p.def.type === 'moving_window'); + } + addSelectPart(selectParts, item, subItem) { - let partModel = sqlPart.create({ type: item.value }); + let partType = item.value; + if (subItem && subItem.type) { + partType = subItem.type; + } + let partModel = sqlPart.create({ type: partType }); if (subItem) { partModel.params = [subItem.value]; } let addAlias = false; - switch (item.value) { + switch (partType) { case 'column': let parts = _.map(selectParts, function(part: any) { return sqlPart.create({ type: part.def.type, params: _.clone(part.params) }); @@ -295,8 +304,10 @@ export class PostgresQueryCtrl extends QueryCtrl { addAlias = true; } break; + case 'moving_window': + partModel.params.push('5'); case 'window': - let windowIndex = _.findIndex(selectParts, (p: any) => p.def.type === 'window'); + let windowIndex = this.findWindowIndex(selectParts); if (windowIndex !== -1) { // replace current window function selectParts[windowIndex] = partModel; diff --git a/public/app/plugins/datasource/postgres/sql_part.ts b/public/app/plugins/datasource/postgres/sql_part.ts index 9cf0bd8f425..52ede10dad9 100644 --- a/public/app/plugins/datasource/postgres/sql_part.ts +++ b/public/app/plugins/datasource/postgres/sql_part.ts @@ -113,6 +113,25 @@ register({ defaultParams: ['increase'], }); +register({ + type: 'moving_window', + style: 'label', + label: 'Moving Window:', + params: [ + { + name: 'function', + type: 'string', + options: ['avg'], + }, + { + name: 'window_size', + type: 'number', + options: ['3', '5', '7', '10', '20'], + }, + ], + defaultParams: ['avg', '5'], +}); + export default { create: createPart, }; From 0ff54d257ade3d5a4fb3369dc2c6509378533a39 Mon Sep 17 00:00:00 2001 From: Leonard Gram Date: Thu, 2 Aug 2018 17:42:28 +0200 Subject: [PATCH 278/786] build: makes it easier to build a local docker container. --- .gitignore | 1 + Makefile | 8 +++++++- packaging/docker/Dockerfile | 9 +++++---- 3 files changed, 13 insertions(+), 5 deletions(-) diff --git a/.gitignore b/.gitignore index 11df66360d9..2484176a469 100644 --- a/.gitignore +++ b/.gitignore @@ -58,6 +58,7 @@ debug.test /examples/*/dist /packaging/**/*.rpm /packaging/**/*.deb +/packaging/**/*.tar.gz # Ignore OSX indexing .DS_Store diff --git a/Makefile b/Makefile index c1d755d247d..9e136688eb7 100644 --- a/Makefile +++ b/Makefile @@ -24,6 +24,12 @@ build-js: build: build-go build-js +build-docker-dev: + @echo "\033[92mInfo:\033[0m the frontend code is expected to be built already." + go run build.go -goos linux -pkg-arch amd64 ${OPT} build package-only latest + cp dist/grafana-latest.linux-x64.tar.gz packaging/docker + cd packaging/docker && docker build --tag grafana/grafana:dev . + test-go: go test -v ./pkg/... @@ -36,4 +42,4 @@ run: ./bin/grafana-server protoc: - protoc -I pkg/tsdb/models pkg/tsdb/models/*.proto --go_out=plugins=grpc:pkg/tsdb/models/. \ No newline at end of file + protoc -I pkg/tsdb/models pkg/tsdb/models/*.proto --go_out=plugins=grpc:pkg/tsdb/models/. diff --git a/packaging/docker/Dockerfile b/packaging/docker/Dockerfile index 3025b03f920..aaaf333fc6b 100644 --- a/packaging/docker/Dockerfile +++ b/packaging/docker/Dockerfile @@ -12,14 +12,15 @@ ENV PATH=/usr/share/grafana/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bi GF_PATHS_PLUGINS="/var/lib/grafana/plugins" \ GF_PATHS_PROVISIONING="/etc/grafana/provisioning" +RUN apt-get update && apt-get install -qq -y tar libfontconfig ca-certificates && \ + apt-get autoremove -y && \ + rm -rf /var/lib/apt/lists/* + COPY ${GRAFANA_TGZ} /tmp/grafana.tar.gz -RUN apt-get update && apt-get install -qq -y tar libfontconfig ca-certificates && \ - mkdir -p "$GF_PATHS_HOME/.aws" && \ +RUN mkdir -p "$GF_PATHS_HOME/.aws" && \ tar xfvz /tmp/grafana.tar.gz --strip-components=1 -C "$GF_PATHS_HOME" && \ rm /tmp/grafana.tar.gz && \ - apt-get autoremove -y && \ - rm -rf /var/lib/apt/lists/* && \ groupadd -r -g $GF_GID grafana && \ useradd -r -u $GF_UID -g grafana grafana && \ mkdir -p "$GF_PATHS_PROVISIONING/datasources" \ From aa830211fe0de7136c4bba834a87a64840395248 Mon Sep 17 00:00:00 2001 From: Sven Klemm Date: Fri, 3 Aug 2018 10:15:28 +0200 Subject: [PATCH 279/786] dont order for aggregate --- public/app/plugins/datasource/postgres/postgres_query.ts | 6 +----- .../datasource/postgres/specs/postgres_query.jest.ts | 4 ++-- 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/public/app/plugins/datasource/postgres/postgres_query.ts b/public/app/plugins/datasource/postgres/postgres_query.ts index d5bdf005e8f..7e49a8a149f 100644 --- a/public/app/plugins/datasource/postgres/postgres_query.ts +++ b/public/app/plugins/datasource/postgres/postgres_query.ts @@ -151,11 +151,7 @@ export default class PostgresQuery { if (func === 'first' || func === 'last') { query = func + '(' + query + ',' + this.target.timeColumn + ')'; } else { - if (windows) { - query = func + '(' + query + ' ORDER BY ' + this.target.timeColumn + ')'; - } else { - query = func + '(' + query + ')'; - } + query = func + '(' + query + ')'; } break; case 'percentile': diff --git a/public/app/plugins/datasource/postgres/specs/postgres_query.jest.ts b/public/app/plugins/datasource/postgres/specs/postgres_query.jest.ts index c1d5400d1fa..1e2b75417da 100644 --- a/public/app/plugins/datasource/postgres/specs/postgres_query.jest.ts +++ b/public/app/plugins/datasource/postgres/specs/postgres_query.jest.ts @@ -98,8 +98,8 @@ describe('PostgresQuery', function() { { type: 'window', params: ['increase'] }, ]; expect(query.buildValueColumn(column)).toBe( - '(CASE WHEN max(v ORDER BY time) >= lag(max(v ORDER BY time)) OVER (PARTITION BY host ORDER BY time) ' + - 'THEN max(v ORDER BY time) - lag(max(v ORDER BY time)) OVER (PARTITION BY host ORDER BY time) ELSE max(v ORDER BY time) END) AS "a"' + '(CASE WHEN max(v) >= lag(max(v)) OVER (PARTITION BY host ORDER BY time) ' + + 'THEN max(v) - lag(max(v)) OVER (PARTITION BY host ORDER BY time) ELSE max(v) END) AS "a"' ); }); From bda49fcaa209f9136659814d6900b3d156c2adca Mon Sep 17 00:00:00 2001 From: David Date: Fri, 3 Aug 2018 10:20:13 +0200 Subject: [PATCH 280/786] Add click on explore table cell to add filter to query (#12729) * Add click on explore table cell to add filter to query - move query state from query row to explore container to be able to set modified queries - added TS interface for columns in table model - plumbing from table cell click to datasource - add modifyQuery to prometheus datasource - implement addFilter as addLabelToQuery with tests * Review feedback - using airbnb style for Cell declaration - fixed addLabelToQuery for complex label values --- public/app/containers/Explore/Explore.tsx | 31 ++++++-- public/app/containers/Explore/QueryRows.tsx | 18 +---- public/app/containers/Explore/Table.tsx | 52 +++++++++++-- public/app/core/table_model.ts | 12 ++- .../datasource/prometheus/datasource.ts | 74 +++++++++++++++++++ .../prometheus/result_transformer.ts | 2 +- .../prometheus/specs/datasource.jest.ts | 27 ++++++- .../specs/result_transformer.jest.ts | 6 +- public/sass/pages/_explore.scss | 4 + 9 files changed, 190 insertions(+), 36 deletions(-) diff --git a/public/app/containers/Explore/Explore.tsx b/public/app/containers/Explore/Explore.tsx index 178e53198d4..a0bb38a13f1 100644 --- a/public/app/containers/Explore/Explore.tsx +++ b/public/app/containers/Explore/Explore.tsx @@ -187,11 +187,14 @@ export class Explore extends React.Component { this.setDatasource(datasource); }; - handleChangeQuery = (query, index) => { + handleChangeQuery = (value, index) => { const { queries } = this.state; + const prevQuery = queries[index]; + const edited = prevQuery.query !== value; const nextQuery = { ...queries[index], - query, + edited, + query: value, }; const nextQueries = [...queries]; nextQueries[index] = nextQuery; @@ -254,6 +257,18 @@ export class Explore extends React.Component { } }; + onClickTableCell = (columnKey: string, rowValue: string) => { + const { datasource, queries } = this.state; + if (datasource && datasource.modifyQuery) { + const nextQueries = queries.map(q => ({ + ...q, + edited: false, + query: datasource.modifyQuery(q.query, { addFilter: { key: columnKey, value: rowValue } }), + })); + this.setState({ queries: nextQueries }, () => this.handleSubmit()); + } + }; + buildQueryOptions(targetOptions: { format: string; instant?: boolean }) { const { datasource, queries, range } = this.state; const resolution = this.el.offsetWidth; @@ -390,12 +405,12 @@ export class Explore extends React.Component { ) : ( -
    - -
    - )} + + )} {!datasourceMissing ? (
    : null} + {supportsTable && showingTable ?
    : null} {supportsLogs && showingLogs ? : null} diff --git a/public/app/containers/Explore/QueryRows.tsx b/public/app/containers/Explore/QueryRows.tsx index 3aaa006d6df..d2c1d81607f 100644 --- a/public/app/containers/Explore/QueryRows.tsx +++ b/public/app/containers/Explore/QueryRows.tsx @@ -3,19 +3,8 @@ import React, { PureComponent } from 'react'; import QueryField from './PromQueryField'; class QueryRow extends PureComponent { - constructor(props) { - super(props); - this.state = { - edited: false, - query: props.query || '', - }; - } - handleChangeQuery = value => { const { index, onChangeQuery } = this.props; - const { query } = this.state; - const edited = query !== value; - this.setState({ edited, query: value }); if (onChangeQuery) { onChangeQuery(value, index); } @@ -43,8 +32,7 @@ class QueryRow extends PureComponent { }; render() { - const { request } = this.props; - const { edited, query } = this.state; + const { request, query, edited } = this.props; return (
    @@ -74,7 +62,9 @@ export default class QueryRows extends PureComponent { const { className = '', queries, ...handlers } = this.props; return (
    - {queries.map((q, index) => )} + {queries.map((q, index) => ( + + ))}
    ); } diff --git a/public/app/containers/Explore/Table.tsx b/public/app/containers/Explore/Table.tsx index 7179a0fc89a..0856acd5d89 100644 --- a/public/app/containers/Explore/Table.tsx +++ b/public/app/containers/Explore/Table.tsx @@ -1,14 +1,44 @@ import React, { PureComponent } from 'react'; -// import TableModel from 'app/core/table_model'; +import TableModel from 'app/core/table_model'; -const EMPTY_TABLE = { - columns: [], - rows: [], -}; +const EMPTY_TABLE = new TableModel(); -export default class Table extends PureComponent { +interface TableProps { + className?: string; + data: TableModel; + onClickCell?: (columnKey: string, rowValue: string) => void; +} + +interface SFCCellProps { + columnIndex: number; + onClickCell?: (columnKey: string, rowValue: string, columnIndex: number, rowIndex: number, table: TableModel) => void; + rowIndex: number; + table: TableModel; + value: string; +} + +function Cell(props: SFCCellProps) { + const { columnIndex, rowIndex, table, value, onClickCell } = props; + const column = table.columns[columnIndex]; + if (column && column.filterable && onClickCell) { + const onClick = event => { + event.preventDefault(); + onClickCell(column.text, value, columnIndex, rowIndex, table); + }; + return ( +
    + ); + } + return ; +} + +export default class Table extends PureComponent { render() { - const { className = '', data } = this.props; + const { className = '', data, onClickCell } = this.props; const tableModel = data || EMPTY_TABLE; return (
    + + {value} + + {value}
    @@ -16,7 +46,13 @@ export default class Table extends PureComponent { {tableModel.columns.map(col => )} - {tableModel.rows.map((row, i) => {row.map((content, j) => )})} + {tableModel.rows.map((row, i) => ( + + {row.map((value, j) => ( + + ))} + + ))}
    {col.text}
    {content}
    ); diff --git a/public/app/core/table_model.ts b/public/app/core/table_model.ts index 04857eb806d..0c85a0293dd 100644 --- a/public/app/core/table_model.ts +++ b/public/app/core/table_model.ts @@ -1,5 +1,15 @@ +interface Column { + text: string; + title?: string; + type?: string; + sort?: boolean; + desc?: boolean; + filterable?: boolean; + unit?: string; +} + export default class TableModel { - columns: any[]; + columns: Column[]; rows: any[]; type: string; columnMap: any; diff --git a/public/app/plugins/datasource/prometheus/datasource.ts b/public/app/plugins/datasource/prometheus/datasource.ts index ac8d774db59..fc8f3999856 100644 --- a/public/app/plugins/datasource/prometheus/datasource.ts +++ b/public/app/plugins/datasource/prometheus/datasource.ts @@ -16,6 +16,72 @@ export function alignRange(start, end, step) { }; } +const keywords = 'by|without|on|ignoring|group_left|group_right'; + +// Duplicate from mode-prometheus.js, which can't be used in tests due to global ace not being loaded. +const builtInWords = [ + keywords, + 'count|count_values|min|max|avg|sum|stddev|stdvar|bottomk|topk|quantile', + 'true|false|null|__name__|job', + 'abs|absent|ceil|changes|clamp_max|clamp_min|count_scalar|day_of_month|day_of_week|days_in_month|delta|deriv', + 'drop_common_labels|exp|floor|histogram_quantile|holt_winters|hour|idelta|increase|irate|label_replace|ln|log2', + 'log10|minute|month|predict_linear|rate|resets|round|scalar|sort|sort_desc|sqrt|time|vector|year|avg_over_time', + 'min_over_time|max_over_time|sum_over_time|count_over_time|quantile_over_time|stddev_over_time|stdvar_over_time', +] + .join('|') + .split('|'); + +// addLabelToQuery('foo', 'bar', 'baz') => 'foo{bar="baz"}' +export function addLabelToQuery(query: string, key: string, value: string): string { + if (!key || !value) { + throw new Error('Need label to add to query.'); + } + + // Add empty selector to bare metric name + let previousWord; + query = query.replace(/(\w+)\b(?![\({=",])/g, (match, word, offset) => { + // Check if inside a selector + const nextSelectorStart = query.slice(offset).indexOf('{'); + const nextSelectorEnd = query.slice(offset).indexOf('}'); + const insideSelector = nextSelectorEnd > -1 && (nextSelectorStart === -1 || nextSelectorStart > nextSelectorEnd); + // Handle "sum by (key) (metric)" + const previousWordIsKeyWord = previousWord && keywords.split('|').indexOf(previousWord) > -1; + previousWord = word; + if (!insideSelector && !previousWordIsKeyWord && builtInWords.indexOf(word) === -1) { + return `${word}{}`; + } + return word; + }); + + // Adding label to existing selectors + const selectorRegexp = /{([^{]*)}/g; + let match = null; + const parts = []; + let lastIndex = 0; + let suffix = ''; + while ((match = selectorRegexp.exec(query))) { + const prefix = query.slice(lastIndex, match.index); + const selectorParts = match[1].split(','); + const labels = selectorParts.reduce((acc, label) => { + const labelParts = label.split('='); + if (labelParts.length === 2) { + acc[labelParts[0]] = labelParts[1]; + } + return acc; + }, {}); + labels[key] = `"${value}"`; + const selector = Object.keys(labels) + .sort() + .map(key => `${key}=${labels[key]}`) + .join(','); + lastIndex = match.index + match[1].length + 2; + suffix = query.slice(match.index + match[0].length); + parts.push(prefix, '{', selector, '}'); + } + parts.push(suffix); + return parts.join(''); +} + export function prometheusRegularEscape(value) { if (typeof value === 'string') { return value.replace(/'/g, "\\\\'"); @@ -384,6 +450,14 @@ export class PrometheusDatasource { return state; } + modifyQuery(query: string, options: any): string { + const { addFilter } = options; + if (addFilter) { + return addLabelToQuery(query, addFilter.key, addFilter.value); + } + return query; + } + getPrometheusTime(date, roundUp) { if (_.isString(date)) { date = dateMath.parse(date, roundUp); diff --git a/public/app/plugins/datasource/prometheus/result_transformer.ts b/public/app/plugins/datasource/prometheus/result_transformer.ts index b6d8a32af5f..a7c6703c10f 100644 --- a/public/app/plugins/datasource/prometheus/result_transformer.ts +++ b/public/app/plugins/datasource/prometheus/result_transformer.ts @@ -86,7 +86,7 @@ export class ResultTransformer { table.columns.push({ text: 'Time', type: 'time' }); _.each(sortedLabels, function(label, labelIndex) { metricLabels[label] = labelIndex + 1; - table.columns.push({ text: label }); + table.columns.push({ text: label, filterable: !label.startsWith('__') }); }); let valueText = resultCount > 1 ? `Value #${refId}` : 'Value'; table.columns.push({ text: valueText }); diff --git a/public/app/plugins/datasource/prometheus/specs/datasource.jest.ts b/public/app/plugins/datasource/prometheus/specs/datasource.jest.ts index aeca8d69191..b946a6f5e7e 100644 --- a/public/app/plugins/datasource/prometheus/specs/datasource.jest.ts +++ b/public/app/plugins/datasource/prometheus/specs/datasource.jest.ts @@ -1,7 +1,14 @@ import _ from 'lodash'; import moment from 'moment'; import q from 'q'; -import { alignRange, PrometheusDatasource, prometheusSpecialRegexEscape, prometheusRegularEscape } from '../datasource'; +import { + alignRange, + PrometheusDatasource, + prometheusSpecialRegexEscape, + prometheusRegularEscape, + addLabelToQuery, +} from '../datasource'; + jest.mock('../metric_find_query'); describe('PrometheusDatasource', () => { @@ -245,6 +252,24 @@ describe('PrometheusDatasource', () => { expect(intervalMs).toEqual({ text: 15000, value: 15000 }); }); }); + + describe('addLabelToQuery()', () => { + expect(() => { + addLabelToQuery('foo', '', ''); + }).toThrow(); + expect(addLabelToQuery('foo + foo', 'bar', 'baz')).toBe('foo{bar="baz"} + foo{bar="baz"}'); + expect(addLabelToQuery('foo{}', 'bar', 'baz')).toBe('foo{bar="baz"}'); + expect(addLabelToQuery('foo{x="yy"}', 'bar', 'baz')).toBe('foo{bar="baz",x="yy"}'); + expect(addLabelToQuery('foo{x="yy"} + metric', 'bar', 'baz')).toBe('foo{bar="baz",x="yy"} + metric{bar="baz"}'); + expect(addLabelToQuery('avg(foo) + sum(xx_yy)', 'bar', 'baz')).toBe('avg(foo{bar="baz"}) + sum(xx_yy{bar="baz"})'); + expect(addLabelToQuery('foo{x="yy"} * metric{y="zz",a="bb"} * metric2', 'bar', 'baz')).toBe( + 'foo{bar="baz",x="yy"} * metric{a="bb",bar="baz",y="zz"} * metric2{bar="baz"}' + ); + expect(addLabelToQuery('sum by (xx) (foo)', 'bar', 'baz')).toBe('sum by (xx) (foo{bar="baz"})'); + expect(addLabelToQuery('foo{instance="my-host.com:9100"}', 'bar', 'baz')).toBe( + 'foo{bar="baz",instance="my-host.com:9100"}' + ); + }); }); const SECOND = 1000; diff --git a/public/app/plugins/datasource/prometheus/specs/result_transformer.jest.ts b/public/app/plugins/datasource/prometheus/specs/result_transformer.jest.ts index c0f2609f5b4..e2a21a8f866 100644 --- a/public/app/plugins/datasource/prometheus/specs/result_transformer.jest.ts +++ b/public/app/plugins/datasource/prometheus/specs/result_transformer.jest.ts @@ -39,7 +39,7 @@ describe('Prometheus Result Transformer', () => { [1443454528000, 'test', '', 'testjob', 3846], [1443454529000, 'test', 'localhost:8080', 'otherjob', 3847], ]); - expect(table.columns).toEqual([ + expect(table.columns).toMatchObject([ { text: 'Time', type: 'time' }, { text: '__name__' }, { text: 'instance' }, @@ -51,7 +51,7 @@ describe('Prometheus Result Transformer', () => { it('should column title include refId if response count is more than 2', () => { var table = ctx.resultTransformer.transformMetricDataToTable(response.data.result, 2, 'B'); expect(table.type).toBe('table'); - expect(table.columns).toEqual([ + expect(table.columns).toMatchObject([ { text: 'Time', type: 'time' }, { text: '__name__' }, { text: 'instance' }, @@ -79,7 +79,7 @@ describe('Prometheus Result Transformer', () => { var table = ctx.resultTransformer.transformMetricDataToTable(response.data.result); expect(table.type).toBe('table'); expect(table.rows).toEqual([[1443454528000, 'test', 'testjob', 3846]]); - expect(table.columns).toEqual([ + expect(table.columns).toMatchObject([ { text: 'Time', type: 'time' }, { text: '__name__' }, { text: 'job' }, diff --git a/public/sass/pages/_explore.scss b/public/sass/pages/_explore.scss index 158f0eb68ad..59b8b62f349 100644 --- a/public/sass/pages/_explore.scss +++ b/public/sass/pages/_explore.scss @@ -80,6 +80,10 @@ .relative { position: relative; } + + .link { + text-decoration: underline; + } } .explore + .explore { From 61e3a0ccebef255c48da2b951f1cfb23628c96a4 Mon Sep 17 00:00:00 2001 From: Tobias Skarhed Date: Fri, 3 Aug 2018 11:57:03 +0200 Subject: [PATCH 281/786] Begin conversion --- public/test/specs/app.jest.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 public/test/specs/app.jest.ts diff --git a/public/test/specs/app.jest.ts b/public/test/specs/app.jest.ts new file mode 100644 index 00000000000..0e15ab8234d --- /dev/null +++ b/public/test/specs/app.jest.ts @@ -0,0 +1,10 @@ +import { GrafanaApp } from 'app/app'; +jest.mock('app/routes/routes'); + +describe('GrafanaApp', () => { + var app = new GrafanaApp(); + + it('can call inits', () => { + expect(app).not.toBe(null); + }); +}); From 5bea54eaaa404f7eef95d798cd87a5c52fae3294 Mon Sep 17 00:00:00 2001 From: Emil Flink Date: Fri, 3 Aug 2018 12:00:20 +0200 Subject: [PATCH 282/786] Support client certificates for LDAP servers --- conf/ldap.toml | 3 +++ docs/sources/installation/ldap.md | 3 +++ pkg/login/ldap.go | 10 ++++++++++ pkg/login/ldap_settings.go | 2 ++ 4 files changed, 18 insertions(+) diff --git a/conf/ldap.toml b/conf/ldap.toml index a74b2b6cc2c..9a7088ed823 100644 --- a/conf/ldap.toml +++ b/conf/ldap.toml @@ -15,6 +15,9 @@ start_tls = false ssl_skip_verify = false # set to the path to your root CA certificate or leave unset to use system defaults # root_ca_cert = "/path/to/certificate.crt" +# Authentication against LDAP servers requiring client certificates +# client_cert = "/path/to/client.crt" +# client_key = "/path/to/client.key" # Search user bind dn bind_dn = "cn=admin,dc=grafana,dc=org" diff --git a/docs/sources/installation/ldap.md b/docs/sources/installation/ldap.md index 9a381b9e467..b555eaf06e0 100644 --- a/docs/sources/installation/ldap.md +++ b/docs/sources/installation/ldap.md @@ -40,6 +40,9 @@ start_tls = false ssl_skip_verify = false # set to the path to your root CA certificate or leave unset to use system defaults # root_ca_cert = "/path/to/certificate.crt" +# Authentication against LDAP servers requiring client certificates +# client_cert = "/path/to/client.crt" +# client_key = "/path/to/client.key" # Search user bind dn bind_dn = "cn=admin,dc=grafana,dc=org" diff --git a/pkg/login/ldap.go b/pkg/login/ldap.go index 9e4918f0290..053778e8deb 100644 --- a/pkg/login/ldap.go +++ b/pkg/login/ldap.go @@ -59,6 +59,13 @@ func (a *ldapAuther) Dial() error { } } } + var clientCert tls.Certificate + if a.server.ClientCert != "" && a.server.ClientKey != "" { + clientCert, err = tls.LoadX509KeyPair(a.server.ClientCert, a.server.ClientKey) + if err != nil { + return err + } + } for _, host := range strings.Split(a.server.Host, " ") { address := fmt.Sprintf("%s:%d", host, a.server.Port) if a.server.UseSSL { @@ -67,6 +74,9 @@ func (a *ldapAuther) Dial() error { ServerName: host, RootCAs: certPool, } + if len(clientCert.Certificate) > 0 { + tlsCfg.Certificates = append(tlsCfg.Certificates, clientCert) + } if a.server.StartTLS { a.conn, err = ldap.Dial("tcp", address) if err == nil { diff --git a/pkg/login/ldap_settings.go b/pkg/login/ldap_settings.go index c4f5982b237..7ebfbc79ba8 100644 --- a/pkg/login/ldap_settings.go +++ b/pkg/login/ldap_settings.go @@ -21,6 +21,8 @@ type LdapServerConf struct { StartTLS bool `toml:"start_tls"` SkipVerifySSL bool `toml:"ssl_skip_verify"` RootCACert string `toml:"root_ca_cert"` + ClientCert string `toml:"client_cert"` + ClientKey string `toml:"client_key"` BindDN string `toml:"bind_dn"` BindPassword string `toml:"bind_password"` Attr LdapAttributeMap `toml:"attributes"` From 61eb96ed79818cb317beeba3f866262807412db3 Mon Sep 17 00:00:00 2001 From: Tobias Skarhed Date: Fri, 3 Aug 2018 12:34:13 +0200 Subject: [PATCH 283/786] Remove simple tests --- .../features/alerting/specs/alert_tab_specs.ts | 17 ----------------- .../dashboard/specs/dashboard_srv_specs.ts | 15 --------------- public/test/specs/app.jest.ts | 10 ---------- public/test/specs/app_specs.ts | 14 -------------- 4 files changed, 56 deletions(-) delete mode 100644 public/app/features/alerting/specs/alert_tab_specs.ts delete mode 100644 public/app/features/dashboard/specs/dashboard_srv_specs.ts delete mode 100644 public/test/specs/app.jest.ts delete mode 100644 public/test/specs/app_specs.ts diff --git a/public/app/features/alerting/specs/alert_tab_specs.ts b/public/app/features/alerting/specs/alert_tab_specs.ts deleted file mode 100644 index 4a4de34fe6c..00000000000 --- a/public/app/features/alerting/specs/alert_tab_specs.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { describe, it, expect } from 'test/lib/common'; - -import { AlertTabCtrl } from '../alert_tab_ctrl'; - -describe('AlertTabCtrl', () => { - var $scope = { - ctrl: {}, - }; - - describe('with null parameters', () => { - it('can be created', () => { - var alertTab = new AlertTabCtrl($scope, null, null, null, null, null); - - expect(alertTab).to.not.be(null); - }); - }); -}); diff --git a/public/app/features/dashboard/specs/dashboard_srv_specs.ts b/public/app/features/dashboard/specs/dashboard_srv_specs.ts deleted file mode 100644 index 0faa7531652..00000000000 --- a/public/app/features/dashboard/specs/dashboard_srv_specs.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { describe, beforeEach, expect } from 'test/lib/common'; - -import { DashboardSrv } from '../dashboard_srv'; - -describe('dashboardSrv', function() { - var _dashboardSrv; - - beforeEach(() => { - _dashboardSrv = new DashboardSrv({}, {}, {}); - }); - - it('should do something', () => { - expect(_dashboardSrv).not.to.be(null); - }); -}); diff --git a/public/test/specs/app.jest.ts b/public/test/specs/app.jest.ts deleted file mode 100644 index 0e15ab8234d..00000000000 --- a/public/test/specs/app.jest.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { GrafanaApp } from 'app/app'; -jest.mock('app/routes/routes'); - -describe('GrafanaApp', () => { - var app = new GrafanaApp(); - - it('can call inits', () => { - expect(app).not.toBe(null); - }); -}); diff --git a/public/test/specs/app_specs.ts b/public/test/specs/app_specs.ts deleted file mode 100644 index f82946c20a2..00000000000 --- a/public/test/specs/app_specs.ts +++ /dev/null @@ -1,14 +0,0 @@ -import {describe, it, expect} from 'test/lib/common'; - -import {GrafanaApp} from 'app/app'; - -describe('GrafanaApp', () => { - - var app = new GrafanaApp(); - - it('can call inits', () => { - expect(app).to.not.be(null); - }); -}); - - From c900a30106b8bc8ac109500d23c209df146a451f Mon Sep 17 00:00:00 2001 From: David Kaltschmidt Date: Fri, 3 Aug 2018 13:09:05 +0200 Subject: [PATCH 284/786] renamed slate unit tests to .jest.ts --- .../Explore/slate-plugins/{braces.test.ts => braces.jest.ts} | 0 .../Explore/slate-plugins/{clear.test.ts => clear.jest.ts} | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename public/app/containers/Explore/slate-plugins/{braces.test.ts => braces.jest.ts} (100%) rename public/app/containers/Explore/slate-plugins/{clear.test.ts => clear.jest.ts} (100%) diff --git a/public/app/containers/Explore/slate-plugins/braces.test.ts b/public/app/containers/Explore/slate-plugins/braces.jest.ts similarity index 100% rename from public/app/containers/Explore/slate-plugins/braces.test.ts rename to public/app/containers/Explore/slate-plugins/braces.jest.ts diff --git a/public/app/containers/Explore/slate-plugins/clear.test.ts b/public/app/containers/Explore/slate-plugins/clear.jest.ts similarity index 100% rename from public/app/containers/Explore/slate-plugins/clear.test.ts rename to public/app/containers/Explore/slate-plugins/clear.jest.ts From e4ae8be9fadd31da7a4bee60a87aba3ecdf6e78b Mon Sep 17 00:00:00 2001 From: Sven Klemm Date: Fri, 3 Aug 2018 14:09:41 +0200 Subject: [PATCH 285/786] fix suggestion query --- public/app/plugins/datasource/postgres/meta_query.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/public/app/plugins/datasource/postgres/meta_query.ts b/public/app/plugins/datasource/postgres/meta_query.ts index fd29121313d..c8a65990e56 100644 --- a/public/app/plugins/datasource/postgres/meta_query.ts +++ b/public/app/plugins/datasource/postgres/meta_query.ts @@ -32,8 +32,8 @@ SELECT FROM information_schema.tables t WHERE table_schema IN ( - SELECT CASE WHEN trim(unnest) = \'"$user"\' THEN user ELSE trim(unnest) END - FROM unnest(string_to_array(current_setting(\'search_path\'),\',\')) + SELECT CASE WHEN trim(unnest) = '"$user"' THEN user ELSE trim(unnest) END + FROM unnest(string_to_array(current_setting('search_path'),',')) ) AND EXISTS ( SELECT 1 @@ -42,7 +42,8 @@ WHERE c.table_schema = t.table_schema AND c.table_name = t.table_name AND udt_name IN ('timestamptz','timestamp') - ) + ) AND + EXISTS ( SELECT 1 FROM information_schema.columns c WHERE From 6d07d825e9cf7f8c1d7807fbdaa2c04d25502b42 Mon Sep 17 00:00:00 2001 From: Sven Klemm Date: Fri, 3 Aug 2018 18:38:40 +0200 Subject: [PATCH 286/786] dont break default parameters for functions --- public/app/plugins/datasource/postgres/query_ctrl.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/public/app/plugins/datasource/postgres/query_ctrl.ts b/public/app/plugins/datasource/postgres/query_ctrl.ts index 6138896d42a..a196859b802 100644 --- a/public/app/plugins/datasource/postgres/query_ctrl.ts +++ b/public/app/plugins/datasource/postgres/query_ctrl.ts @@ -275,7 +275,7 @@ export class PostgresQueryCtrl extends QueryCtrl { } let partModel = sqlPart.create({ type: partType }); if (subItem) { - partModel.params = [subItem.value]; + partModel.params[0] = subItem.value; } let addAlias = false; @@ -287,7 +287,6 @@ export class PostgresQueryCtrl extends QueryCtrl { this.selectParts.push(parts); break; case 'percentile': - partModel.params.push('0.95'); case 'aggregate': // add group by if no group by yet if (this.target.group.length === 0) { @@ -305,7 +304,6 @@ export class PostgresQueryCtrl extends QueryCtrl { } break; case 'moving_window': - partModel.params.push('5'); case 'window': let windowIndex = this.findWindowIndex(selectParts); if (windowIndex !== -1) { From 818fe09a7f94e7b6c4ad7b36b1ce8f3348ef7598 Mon Sep 17 00:00:00 2001 From: Tobias Skarhed Date: Thu, 2 Aug 2018 13:39:05 +0200 Subject: [PATCH 287/786] Fit panels to screen height --- .../app/features/dashboard/dashboard_ctrl.ts | 30 ++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/public/app/features/dashboard/dashboard_ctrl.ts b/public/app/features/dashboard/dashboard_ctrl.ts index 94d0b18f157..77e5ca88552 100644 --- a/public/app/features/dashboard/dashboard_ctrl.ts +++ b/public/app/features/dashboard/dashboard_ctrl.ts @@ -4,7 +4,8 @@ import coreModule from 'app/core/core_module'; import { PanelContainer } from './dashgrid/PanelContainer'; import { DashboardModel } from './dashboard_model'; import { PanelModel } from './panel_model'; - +import { GRID_CELL_HEIGHT } from 'app/core/constants'; +import { PanelLinksEditorCtrl } from '../panellinks/module'; export class DashboardCtrl implements PanelContainer { dashboard: DashboardModel; dashboardViewState: any; @@ -62,6 +63,33 @@ export class DashboardCtrl implements PanelContainer { .finally(() => { this.dashboard = dashboard; this.dashboard.processRepeats(); + console.log(this.dashboard.panels); + + let maxRows = Math.max( + ...this.dashboard.panels.map(panel => { + return panel.gridPos.h + panel.gridPos.y; + }) + ); + console.log('maxRows: ' + maxRows); + //Consider navbar and submenu controls + let availableHeight = window.innerHeight - 280; + let availableRows = Math.floor(availableHeight / GRID_CELL_HEIGHT); + + console.log('availableRows: ' + availableRows); + if (maxRows > availableRows) { + let scaleFactor = maxRows / availableRows; + console.log(scaleFactor); + + this.dashboard.panels.forEach((panel, i) => { + console.log(i); + console.log(panel.gridPos); + panel.gridPos.y = Math.floor(panel.gridPos.y / scaleFactor) || 1; + panel.gridPos.h = Math.floor(panel.gridPos.h / scaleFactor) || 1; + + console.log(panel.gridPos); + }); + } + console.log(this.dashboard.panels); this.unsavedChangesSrv.init(dashboard, this.$scope); From a9f24bb36d487e7880864e65aaf26c277db11313 Mon Sep 17 00:00:00 2001 From: Tobias Skarhed Date: Thu, 2 Aug 2018 13:52:10 +0200 Subject: [PATCH 288/786] Remove weird import --- public/app/features/dashboard/dashboard_ctrl.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/app/features/dashboard/dashboard_ctrl.ts b/public/app/features/dashboard/dashboard_ctrl.ts index 77e5ca88552..04f1207d49a 100644 --- a/public/app/features/dashboard/dashboard_ctrl.ts +++ b/public/app/features/dashboard/dashboard_ctrl.ts @@ -5,7 +5,7 @@ import { PanelContainer } from './dashgrid/PanelContainer'; import { DashboardModel } from './dashboard_model'; import { PanelModel } from './panel_model'; import { GRID_CELL_HEIGHT } from 'app/core/constants'; -import { PanelLinksEditorCtrl } from '../panellinks/module'; + export class DashboardCtrl implements PanelContainer { dashboard: DashboardModel; dashboardViewState: any; From 78b3dc40f11fc8e3ff5d5094943c57502fc49aa7 Mon Sep 17 00:00:00 2001 From: Tobias Skarhed Date: Thu, 2 Aug 2018 14:35:49 +0200 Subject: [PATCH 289/786] Add margin and padding compensation --- .../app/features/dashboard/dashboard_ctrl.ts | 29 +++++++++---------- 1 file changed, 14 insertions(+), 15 deletions(-) diff --git a/public/app/features/dashboard/dashboard_ctrl.ts b/public/app/features/dashboard/dashboard_ctrl.ts index 04f1207d49a..a3f9a7b33d5 100644 --- a/public/app/features/dashboard/dashboard_ctrl.ts +++ b/public/app/features/dashboard/dashboard_ctrl.ts @@ -4,7 +4,7 @@ import coreModule from 'app/core/core_module'; import { PanelContainer } from './dashgrid/PanelContainer'; import { DashboardModel } from './dashboard_model'; import { PanelModel } from './panel_model'; -import { GRID_CELL_HEIGHT } from 'app/core/constants'; +import { GRID_CELL_HEIGHT, GRID_CELL_VMARGIN } from 'app/core/constants'; export class DashboardCtrl implements PanelContainer { dashboard: DashboardModel; @@ -71,24 +71,23 @@ export class DashboardCtrl implements PanelContainer { }) ); console.log('maxRows: ' + maxRows); - //Consider navbar and submenu controls - let availableHeight = window.innerHeight - 280; - let availableRows = Math.floor(availableHeight / GRID_CELL_HEIGHT); + //Consider navbar and submenu controls, padding and margin + let availableHeight = window.innerHeight - 80 - 2 * GRID_CELL_VMARGIN; + let availableRows = Math.floor(availableHeight / (GRID_CELL_HEIGHT + GRID_CELL_VMARGIN)); console.log('availableRows: ' + availableRows); - if (maxRows > availableRows) { - let scaleFactor = maxRows / availableRows; - console.log(scaleFactor); + let scaleFactor = maxRows / availableRows; + console.log(scaleFactor); - this.dashboard.panels.forEach((panel, i) => { - console.log(i); - console.log(panel.gridPos); - panel.gridPos.y = Math.floor(panel.gridPos.y / scaleFactor) || 1; - panel.gridPos.h = Math.floor(panel.gridPos.h / scaleFactor) || 1; + this.dashboard.panels.forEach((panel, i) => { + console.log(i); + console.log(panel.gridPos); + panel.gridPos.y = Math.floor(panel.gridPos.y / scaleFactor) || 1; + panel.gridPos.h = Math.floor(panel.gridPos.h / scaleFactor) || 1; + + console.log(panel.gridPos); + }); - console.log(panel.gridPos); - }); - } console.log(this.dashboard.panels); this.unsavedChangesSrv.init(dashboard, this.$scope); From 9e4748e2aa5eff942c49d74d807063fee6b9e612 Mon Sep 17 00:00:00 2001 From: Tobias Skarhed Date: Thu, 2 Aug 2018 14:46:02 +0200 Subject: [PATCH 290/786] Go with just single margin compensation --- public/app/features/dashboard/dashboard_ctrl.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/app/features/dashboard/dashboard_ctrl.ts b/public/app/features/dashboard/dashboard_ctrl.ts index a3f9a7b33d5..51a42ab0b0a 100644 --- a/public/app/features/dashboard/dashboard_ctrl.ts +++ b/public/app/features/dashboard/dashboard_ctrl.ts @@ -72,7 +72,7 @@ export class DashboardCtrl implements PanelContainer { ); console.log('maxRows: ' + maxRows); //Consider navbar and submenu controls, padding and margin - let availableHeight = window.innerHeight - 80 - 2 * GRID_CELL_VMARGIN; + let availableHeight = window.innerHeight - 80; let availableRows = Math.floor(availableHeight / (GRID_CELL_HEIGHT + GRID_CELL_VMARGIN)); console.log('availableRows: ' + availableRows); From 338a37abc8d2dd59d2c470cd7bffe67c31f4caf0 Mon Sep 17 00:00:00 2001 From: Tobias Skarhed Date: Thu, 2 Aug 2018 15:06:41 +0200 Subject: [PATCH 291/786] Replace floor with round --- public/app/features/dashboard/dashboard_ctrl.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/public/app/features/dashboard/dashboard_ctrl.ts b/public/app/features/dashboard/dashboard_ctrl.ts index 51a42ab0b0a..bdd6d7050c5 100644 --- a/public/app/features/dashboard/dashboard_ctrl.ts +++ b/public/app/features/dashboard/dashboard_ctrl.ts @@ -82,8 +82,8 @@ export class DashboardCtrl implements PanelContainer { this.dashboard.panels.forEach((panel, i) => { console.log(i); console.log(panel.gridPos); - panel.gridPos.y = Math.floor(panel.gridPos.y / scaleFactor) || 1; - panel.gridPos.h = Math.floor(panel.gridPos.h / scaleFactor) || 1; + panel.gridPos.y = Math.round(panel.gridPos.y / scaleFactor) || 1; + panel.gridPos.h = Math.round(panel.gridPos.h / scaleFactor) || 1; console.log(panel.gridPos); }); From 63fa9fdc6d3fcfb8fd31450bb9f5880cd497fdf9 Mon Sep 17 00:00:00 2001 From: Tobias Skarhed Date: Thu, 2 Aug 2018 15:55:03 +0200 Subject: [PATCH 292/786] Add temporary url parameter --- .../app/features/dashboard/dashboard_ctrl.ts | 38 ++++++++----------- 1 file changed, 15 insertions(+), 23 deletions(-) diff --git a/public/app/features/dashboard/dashboard_ctrl.ts b/public/app/features/dashboard/dashboard_ctrl.ts index bdd6d7050c5..ef68a76da30 100644 --- a/public/app/features/dashboard/dashboard_ctrl.ts +++ b/public/app/features/dashboard/dashboard_ctrl.ts @@ -63,32 +63,24 @@ export class DashboardCtrl implements PanelContainer { .finally(() => { this.dashboard = dashboard; this.dashboard.processRepeats(); - console.log(this.dashboard.panels); - let maxRows = Math.max( - ...this.dashboard.panels.map(panel => { - return panel.gridPos.h + panel.gridPos.y; - }) - ); - console.log('maxRows: ' + maxRows); - //Consider navbar and submenu controls, padding and margin - let availableHeight = window.innerHeight - 80; - let availableRows = Math.floor(availableHeight / (GRID_CELL_HEIGHT + GRID_CELL_VMARGIN)); + if (window.location.search.search('autofitpanels') !== -1) { + let maxRows = Math.max( + ...this.dashboard.panels.map(panel => { + return panel.gridPos.h + panel.gridPos.y; + }) + ); - console.log('availableRows: ' + availableRows); - let scaleFactor = maxRows / availableRows; - console.log(scaleFactor); + //Consider navbar and submenu controls, padding and margin + let availableHeight = window.innerHeight - 80; + let availableRows = Math.floor(availableHeight / (GRID_CELL_HEIGHT + GRID_CELL_VMARGIN)); + let scaleFactor = maxRows / availableRows; - this.dashboard.panels.forEach((panel, i) => { - console.log(i); - console.log(panel.gridPos); - panel.gridPos.y = Math.round(panel.gridPos.y / scaleFactor) || 1; - panel.gridPos.h = Math.round(panel.gridPos.h / scaleFactor) || 1; - - console.log(panel.gridPos); - }); - - console.log(this.dashboard.panels); + this.dashboard.panels.forEach((panel, i) => { + panel.gridPos.y = Math.round(panel.gridPos.y / scaleFactor) || 1; + panel.gridPos.h = Math.round(panel.gridPos.h / scaleFactor) || 1; + }); + } this.unsavedChangesSrv.init(dashboard, this.$scope); From 1618b095c7701015a82ae4e4aeb597bec7007d24 Mon Sep 17 00:00:00 2001 From: Tobias Skarhed Date: Fri, 3 Aug 2018 11:00:27 +0200 Subject: [PATCH 293/786] Use and add keybard shortcut --- public/app/core/services/keybindingSrv.ts | 16 +++++++++++++++- public/app/features/dashboard/dashboard_ctrl.ts | 5 +++-- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/public/app/core/services/keybindingSrv.ts b/public/app/core/services/keybindingSrv.ts index 672ae29740b..0930a16d797 100644 --- a/public/app/core/services/keybindingSrv.ts +++ b/public/app/core/services/keybindingSrv.ts @@ -15,7 +15,14 @@ export class KeybindingSrv { timepickerOpen = false; /** @ngInject */ - constructor(private $rootScope, private $location, private datasourceSrv, private timeSrv, private contextSrv) { + constructor( + private $rootScope, + private $location, + private datasourceSrv, + private timeSrv, + private contextSrv, + private $window + ) { // clear out all shortcuts on route change $rootScope.$on('$routeChangeSuccess', () => { Mousetrap.reset(); @@ -259,6 +266,13 @@ export class KeybindingSrv { this.bind('d v', () => { appEvents.emit('toggle-view-mode'); }); + + //Autofit panels + this.bind('d a', () => { + this.$location.search('autofitpanels', this.$location.search().autofitpanels ? null : true); + //Force reload + this.$window.location.href = this.$location.absUrl(); + }); } } diff --git a/public/app/features/dashboard/dashboard_ctrl.ts b/public/app/features/dashboard/dashboard_ctrl.ts index ef68a76da30..ccb5686b23a 100644 --- a/public/app/features/dashboard/dashboard_ctrl.ts +++ b/public/app/features/dashboard/dashboard_ctrl.ts @@ -24,7 +24,8 @@ export class DashboardCtrl implements PanelContainer { private unsavedChangesSrv, private dashboardViewStateSrv, public playlistSrv, - private panelLoader + private panelLoader, + private $location ) { // temp hack due to way dashboards are loaded // can't use controllerAs on route yet @@ -64,7 +65,7 @@ export class DashboardCtrl implements PanelContainer { this.dashboard = dashboard; this.dashboard.processRepeats(); - if (window.location.search.search('autofitpanels') !== -1) { + if (this.$location.search().autofitpanels) { let maxRows = Math.max( ...this.dashboard.panels.map(panel => { return panel.gridPos.h + panel.gridPos.y; From 36c406eefb89d1963bcf54015b326b0cae3f5f0a Mon Sep 17 00:00:00 2001 From: Tobias Skarhed Date: Fri, 3 Aug 2018 11:37:33 +0200 Subject: [PATCH 294/786] Extract to own method --- .../app/features/dashboard/dashboard_ctrl.ts | 39 ++++++++++--------- 1 file changed, 21 insertions(+), 18 deletions(-) diff --git a/public/app/features/dashboard/dashboard_ctrl.ts b/public/app/features/dashboard/dashboard_ctrl.ts index ccb5686b23a..16b21306a4e 100644 --- a/public/app/features/dashboard/dashboard_ctrl.ts +++ b/public/app/features/dashboard/dashboard_ctrl.ts @@ -65,24 +65,7 @@ export class DashboardCtrl implements PanelContainer { this.dashboard = dashboard; this.dashboard.processRepeats(); - if (this.$location.search().autofitpanels) { - let maxRows = Math.max( - ...this.dashboard.panels.map(panel => { - return panel.gridPos.h + panel.gridPos.y; - }) - ); - - //Consider navbar and submenu controls, padding and margin - let availableHeight = window.innerHeight - 80; - let availableRows = Math.floor(availableHeight / (GRID_CELL_HEIGHT + GRID_CELL_VMARGIN)); - let scaleFactor = maxRows / availableRows; - - this.dashboard.panels.forEach((panel, i) => { - panel.gridPos.y = Math.round(panel.gridPos.y / scaleFactor) || 1; - panel.gridPos.h = Math.round(panel.gridPos.h / scaleFactor) || 1; - }); - } - + this.autofitPanels(); this.unsavedChangesSrv.init(dashboard, this.$scope); // TODO refactor ViewStateSrv @@ -99,6 +82,26 @@ export class DashboardCtrl implements PanelContainer { .catch(this.onInitFailed.bind(this, 'Dashboard init failed', true)); } + autofitPanels() { + if (this.$location.search().autofitpanels) { + let maxRows = Math.max( + ...this.dashboard.panels.map(panel => { + return panel.gridPos.h + panel.gridPos.y; + }) + ); + + //Consider navbar and submenu controls, padding and margin + let availableHeight = window.innerHeight - 80; + let availableRows = Math.floor(availableHeight / (GRID_CELL_HEIGHT + GRID_CELL_VMARGIN)); + let scaleFactor = maxRows / availableRows; + + this.dashboard.panels.forEach((panel, i) => { + panel.gridPos.y = Math.round(panel.gridPos.y / scaleFactor) || 1; + panel.gridPos.h = Math.round(panel.gridPos.h / scaleFactor) || 1; + }); + } + } + onInitFailed(msg, fatal, err) { console.log(msg, err); From 4b84a585751fe955a039c2b910aff1b5b6385340 Mon Sep 17 00:00:00 2001 From: Tobias Skarhed Date: Fri, 3 Aug 2018 12:19:41 +0200 Subject: [PATCH 295/786] Disable submenu when autopanels is enabled --- public/app/features/dashboard/dashboard_ctrl.ts | 4 +++- public/app/features/dashboard/dashboard_model.ts | 5 +++++ .../dashboard/specs/dashboard_model.jest.ts | 13 +++++++++++++ 3 files changed, 21 insertions(+), 1 deletion(-) diff --git a/public/app/features/dashboard/dashboard_ctrl.ts b/public/app/features/dashboard/dashboard_ctrl.ts index 16b21306a4e..9266794d6e4 100644 --- a/public/app/features/dashboard/dashboard_ctrl.ts +++ b/public/app/features/dashboard/dashboard_ctrl.ts @@ -91,7 +91,7 @@ export class DashboardCtrl implements PanelContainer { ); //Consider navbar and submenu controls, padding and margin - let availableHeight = window.innerHeight - 80; + let availableHeight = window.innerHeight - 40; let availableRows = Math.floor(availableHeight / (GRID_CELL_HEIGHT + GRID_CELL_VMARGIN)); let scaleFactor = maxRows / availableRows; @@ -99,6 +99,8 @@ export class DashboardCtrl implements PanelContainer { panel.gridPos.y = Math.round(panel.gridPos.y / scaleFactor) || 1; panel.gridPos.h = Math.round(panel.gridPos.h / scaleFactor) || 1; }); + this.dashboard.meta.autofitpanels = true; + console.log(this.dashboard); } } diff --git a/public/app/features/dashboard/dashboard_model.ts b/public/app/features/dashboard/dashboard_model.ts index 976e4213920..23a43d80353 100644 --- a/public/app/features/dashboard/dashboard_model.ts +++ b/public/app/features/dashboard/dashboard_model.ts @@ -9,6 +9,7 @@ import sortByKeys from 'app/core/utils/sort_by_keys'; import { PanelModel } from './panel_model'; import { DashboardMigrator } from './dashboard_migration'; +import { tickStep } from '../../core/utils/ticks'; export class DashboardModel { id: any; @@ -591,6 +592,10 @@ export class DashboardModel { updateSubmenuVisibility() { this.meta.submenuEnabled = (() => { + if (this.meta.autofitpanels) { + return false; + } + if (this.links.length > 0) { return true; } diff --git a/public/app/features/dashboard/specs/dashboard_model.jest.ts b/public/app/features/dashboard/specs/dashboard_model.jest.ts index 6ac642cd58e..adbdd37c893 100644 --- a/public/app/features/dashboard/specs/dashboard_model.jest.ts +++ b/public/app/features/dashboard/specs/dashboard_model.jest.ts @@ -305,6 +305,19 @@ describe('DashboardModel', function() { }); }); + describe('updateSubmenuVisibility with autofitpanels enabled', function() { + var model; + + beforeEach(function() { + model = new DashboardModel({}, { autofitpanels: true }); + model.updateSubmenuVisibility(); + }); + + it('should not enable submmenu', function() { + expect(model.meta.submenuEnabled).toBe(false); + }); + }); + describe('updateSubmenuVisibility with hidden annotation toggle', function() { var dashboard; From f00b5eee83d606642412592fe75c348aeea7abc7 Mon Sep 17 00:00:00 2001 From: Tobias Skarhed Date: Fri, 3 Aug 2018 12:20:46 +0200 Subject: [PATCH 296/786] Remove weird import --- public/app/features/dashboard/dashboard_model.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/public/app/features/dashboard/dashboard_model.ts b/public/app/features/dashboard/dashboard_model.ts index 23a43d80353..5a2310ac04b 100644 --- a/public/app/features/dashboard/dashboard_model.ts +++ b/public/app/features/dashboard/dashboard_model.ts @@ -9,7 +9,6 @@ import sortByKeys from 'app/core/utils/sort_by_keys'; import { PanelModel } from './panel_model'; import { DashboardMigrator } from './dashboard_migration'; -import { tickStep } from '../../core/utils/ticks'; export class DashboardModel { id: any; From 013f8cd8ea86fe94ec4856a401a81e4042b0ed7a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Sun, 5 Aug 2018 11:04:12 +0200 Subject: [PATCH 297/786] refactor: moving code around a bit, refactoring PR #12796 --- .../app/features/dashboard/dashboard_ctrl.ts | 28 ++----------------- .../app/features/dashboard/dashboard_model.ts | 28 +++++++++++++++---- .../dashboard/specs/dashboard_model.jest.ts | 13 --------- public/app/routes/dashboard_loaders.ts | 5 ++-- public/sass/pages/_dashboard.scss | 2 +- 5 files changed, 29 insertions(+), 47 deletions(-) diff --git a/public/app/features/dashboard/dashboard_ctrl.ts b/public/app/features/dashboard/dashboard_ctrl.ts index 9266794d6e4..cc318be4939 100644 --- a/public/app/features/dashboard/dashboard_ctrl.ts +++ b/public/app/features/dashboard/dashboard_ctrl.ts @@ -4,7 +4,6 @@ import coreModule from 'app/core/core_module'; import { PanelContainer } from './dashgrid/PanelContainer'; import { DashboardModel } from './dashboard_model'; import { PanelModel } from './panel_model'; -import { GRID_CELL_HEIGHT, GRID_CELL_VMARGIN } from 'app/core/constants'; export class DashboardCtrl implements PanelContainer { dashboard: DashboardModel; @@ -24,8 +23,7 @@ export class DashboardCtrl implements PanelContainer { private unsavedChangesSrv, private dashboardViewStateSrv, public playlistSrv, - private panelLoader, - private $location + private panelLoader ) { // temp hack due to way dashboards are loaded // can't use controllerAs on route yet @@ -64,8 +62,8 @@ export class DashboardCtrl implements PanelContainer { .finally(() => { this.dashboard = dashboard; this.dashboard.processRepeats(); + this.dashboard.autoFitPanels(window.innerHeight); - this.autofitPanels(); this.unsavedChangesSrv.init(dashboard, this.$scope); // TODO refactor ViewStateSrv @@ -82,28 +80,6 @@ export class DashboardCtrl implements PanelContainer { .catch(this.onInitFailed.bind(this, 'Dashboard init failed', true)); } - autofitPanels() { - if (this.$location.search().autofitpanels) { - let maxRows = Math.max( - ...this.dashboard.panels.map(panel => { - return panel.gridPos.h + panel.gridPos.y; - }) - ); - - //Consider navbar and submenu controls, padding and margin - let availableHeight = window.innerHeight - 40; - let availableRows = Math.floor(availableHeight / (GRID_CELL_HEIGHT + GRID_CELL_VMARGIN)); - let scaleFactor = maxRows / availableRows; - - this.dashboard.panels.forEach((panel, i) => { - panel.gridPos.y = Math.round(panel.gridPos.y / scaleFactor) || 1; - panel.gridPos.h = Math.round(panel.gridPos.h / scaleFactor) || 1; - }); - this.dashboard.meta.autofitpanels = true; - console.log(this.dashboard); - } - } - onInitFailed(msg, fatal, err) { console.log(msg, err); diff --git a/public/app/features/dashboard/dashboard_model.ts b/public/app/features/dashboard/dashboard_model.ts index 5a2310ac04b..5332357570b 100644 --- a/public/app/features/dashboard/dashboard_model.ts +++ b/public/app/features/dashboard/dashboard_model.ts @@ -1,7 +1,7 @@ import moment from 'moment'; import _ from 'lodash'; -import { GRID_COLUMN_COUNT, REPEAT_DIR_VERTICAL } from 'app/core/constants'; +import { GRID_COLUMN_COUNT, REPEAT_DIR_VERTICAL, GRID_CELL_HEIGHT, GRID_CELL_VMARGIN } from 'app/core/constants'; import { DEFAULT_ANNOTATION_COLOR } from 'app/core/utils/colors'; import { Emitter } from 'app/core/utils/emitter'; import { contextSrv } from 'app/core/services/context_srv'; @@ -591,10 +591,6 @@ export class DashboardModel { updateSubmenuVisibility() { this.meta.submenuEnabled = (() => { - if (this.meta.autofitpanels) { - return false; - } - if (this.links.length > 0) { return true; } @@ -834,4 +830,26 @@ export class DashboardModel { return !_.isEqual(updated, this.originalTemplating); } + + autoFitPanels(viewHeight: number) { + if (!this.meta.autofitpanels) { + return; + } + + let maxRows = Math.max( + ...this.panels.map(panel => { + return panel.gridPos.h + panel.gridPos.y; + }) + ); + + //Consider navbar and submenu controls, padding and margin + let availableHeight = window.innerHeight - 55 - 20; + let availableRows = Math.floor(availableHeight / (GRID_CELL_HEIGHT + GRID_CELL_VMARGIN)); + let scaleFactor = maxRows / availableRows; + + this.panels.forEach((panel, i) => { + panel.gridPos.y = Math.round(panel.gridPos.y / scaleFactor) || 1; + panel.gridPos.h = Math.round(panel.gridPos.h / scaleFactor) || 1; + }); + } } diff --git a/public/app/features/dashboard/specs/dashboard_model.jest.ts b/public/app/features/dashboard/specs/dashboard_model.jest.ts index adbdd37c893..6ac642cd58e 100644 --- a/public/app/features/dashboard/specs/dashboard_model.jest.ts +++ b/public/app/features/dashboard/specs/dashboard_model.jest.ts @@ -305,19 +305,6 @@ describe('DashboardModel', function() { }); }); - describe('updateSubmenuVisibility with autofitpanels enabled', function() { - var model; - - beforeEach(function() { - model = new DashboardModel({}, { autofitpanels: true }); - model.updateSubmenuVisibility(); - }); - - it('should not enable submmenu', function() { - expect(model.meta.submenuEnabled).toBe(false); - }); - }); - describe('updateSubmenuVisibility with hidden annotation toggle', function() { var dashboard; diff --git a/public/app/routes/dashboard_loaders.ts b/public/app/routes/dashboard_loaders.ts index 9224ec33bcc..3642b54c790 100644 --- a/public/app/routes/dashboard_loaders.ts +++ b/public/app/routes/dashboard_loaders.ts @@ -38,9 +38,10 @@ export class LoadDashboardCtrl { } } - if ($routeParams.keepRows) { - result.meta.keepRows = true; + if ($routeParams.autofitpanels) { + result.meta.autofitpanels = true; } + $scope.initDashboard(result, $scope); }); } diff --git a/public/sass/pages/_dashboard.scss b/public/sass/pages/_dashboard.scss index 970b625c4f8..6225f840973 100644 --- a/public/sass/pages/_dashboard.scss +++ b/public/sass/pages/_dashboard.scss @@ -1,5 +1,5 @@ .dashboard-container { - padding: $dashboard-padding; + padding: $dashboard-padding $dashboard-padding 0 $dashboard-padding; width: 100%; min-height: 100%; } From b1b8a380615d508659b3000141dcaee66d31f723 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Sun, 5 Aug 2018 11:18:49 +0200 Subject: [PATCH 298/786] refactor: renaming variables, refactoring PR #12796 --- public/app/features/dashboard/dashboard_model.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/public/app/features/dashboard/dashboard_model.ts b/public/app/features/dashboard/dashboard_model.ts index 5332357570b..d82492f753b 100644 --- a/public/app/features/dashboard/dashboard_model.ts +++ b/public/app/features/dashboard/dashboard_model.ts @@ -836,16 +836,16 @@ export class DashboardModel { return; } - let maxRows = Math.max( + let currentGridHeight = Math.max( ...this.panels.map(panel => { return panel.gridPos.h + panel.gridPos.y; }) ); //Consider navbar and submenu controls, padding and margin - let availableHeight = window.innerHeight - 55 - 20; - let availableRows = Math.floor(availableHeight / (GRID_CELL_HEIGHT + GRID_CELL_VMARGIN)); - let scaleFactor = maxRows / availableRows; + let visibleHeight = window.innerHeight - 55 - 20; + let visibleGridHeight = Math.floor(visibleHeight / (GRID_CELL_HEIGHT + GRID_CELL_VMARGIN)); + let scaleFactor = currentGridHeight / visibleGridHeight; this.panels.forEach((panel, i) => { panel.gridPos.y = Math.round(panel.gridPos.y / scaleFactor) || 1; From 624f3a0173a46d9016f08a6959c1c00363bac80e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Sun, 5 Aug 2018 11:27:02 +0200 Subject: [PATCH 299/786] refactor: take submenu into account PR #12796 --- public/app/features/dashboard/dashboard_ctrl.ts | 3 +-- public/app/features/dashboard/dashboard_model.ts | 14 ++++++++++---- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/public/app/features/dashboard/dashboard_ctrl.ts b/public/app/features/dashboard/dashboard_ctrl.ts index cc318be4939..c6bb6492172 100644 --- a/public/app/features/dashboard/dashboard_ctrl.ts +++ b/public/app/features/dashboard/dashboard_ctrl.ts @@ -62,6 +62,7 @@ export class DashboardCtrl implements PanelContainer { .finally(() => { this.dashboard = dashboard; this.dashboard.processRepeats(); + this.dashboard.updateSubmenuVisibility(); this.dashboard.autoFitPanels(window.innerHeight); this.unsavedChangesSrv.init(dashboard, this.$scope); @@ -71,8 +72,6 @@ export class DashboardCtrl implements PanelContainer { this.dashboardViewState = this.dashboardViewStateSrv.create(this.$scope); this.keybindingSrv.setupDashboardBindings(this.$scope, dashboard); - - this.dashboard.updateSubmenuVisibility(); this.setWindowTitleAndTheme(); this.$scope.appEvent('dashboard-initialized', dashboard); diff --git a/public/app/features/dashboard/dashboard_model.ts b/public/app/features/dashboard/dashboard_model.ts index d82492f753b..92392fc80e8 100644 --- a/public/app/features/dashboard/dashboard_model.ts +++ b/public/app/features/dashboard/dashboard_model.ts @@ -836,16 +836,22 @@ export class DashboardModel { return; } - let currentGridHeight = Math.max( + const currentGridHeight = Math.max( ...this.panels.map(panel => { return panel.gridPos.h + panel.gridPos.y; }) ); - //Consider navbar and submenu controls, padding and margin + // Consider navbar and submenu controls, padding and margin let visibleHeight = window.innerHeight - 55 - 20; - let visibleGridHeight = Math.floor(visibleHeight / (GRID_CELL_HEIGHT + GRID_CELL_VMARGIN)); - let scaleFactor = currentGridHeight / visibleGridHeight; + + // Remove submenu if visible + if (this.meta.submenuEnabled) { + visibleHeight -= 50; + } + + const visibleGridHeight = Math.floor(visibleHeight / (GRID_CELL_HEIGHT + GRID_CELL_VMARGIN)); + const scaleFactor = currentGridHeight / visibleGridHeight; this.panels.forEach((panel, i) => { panel.gridPos.y = Math.round(panel.gridPos.y / scaleFactor) || 1; From 45eadae6923a903a4ff1a1709f8c8a6a30ddf7a6 Mon Sep 17 00:00:00 2001 From: Tobias Skarhed Date: Mon, 6 Aug 2018 10:42:35 +0200 Subject: [PATCH 300/786] Convert datasource --- .../opentsdb/specs/datasource-specs.ts | 105 ------------------ .../opentsdb/specs/datasource.jest.ts | 91 +++++++++++++++ 2 files changed, 91 insertions(+), 105 deletions(-) delete mode 100644 public/app/plugins/datasource/opentsdb/specs/datasource-specs.ts create mode 100644 public/app/plugins/datasource/opentsdb/specs/datasource.jest.ts diff --git a/public/app/plugins/datasource/opentsdb/specs/datasource-specs.ts b/public/app/plugins/datasource/opentsdb/specs/datasource-specs.ts deleted file mode 100644 index a4c90af3d50..00000000000 --- a/public/app/plugins/datasource/opentsdb/specs/datasource-specs.ts +++ /dev/null @@ -1,105 +0,0 @@ -import { describe, beforeEach, it, expect, angularMocks } from 'test/lib/common'; -import helpers from 'test/specs/helpers'; -import OpenTsDatasource from '../datasource'; - -describe('opentsdb', function() { - var ctx = new helpers.ServiceTestContext(); - var instanceSettings = { url: '', jsonData: { tsdbVersion: 1 } }; - - 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(OpenTsDatasource, { - instanceSettings: instanceSettings, - }); - $httpBackend.when('GET', /\.html$/).respond(''); - }) - ); - - describe('When performing metricFindQuery', function() { - var results; - var requestOptions; - - beforeEach(function() { - ctx.backendSrv.datasourceRequest = function(options) { - requestOptions = options; - return ctx.$q.when({ - data: [{ target: 'prod1.count', datapoints: [[10, 1], [12, 1]] }], - }); - }; - }); - - it('metrics() should generate api suggest query', function() { - ctx.ds.metricFindQuery('metrics(pew)').then(function(data) { - results = data; - }); - ctx.$rootScope.$apply(); - expect(requestOptions.url).to.be('/api/suggest'); - expect(requestOptions.params.type).to.be('metrics'); - expect(requestOptions.params.q).to.be('pew'); - expect(results).not.to.be(null); - }); - - it('tag_names(cpu) should generate lookup query', function() { - ctx.ds.metricFindQuery('tag_names(cpu)').then(function(data) { - results = data; - }); - ctx.$rootScope.$apply(); - expect(requestOptions.url).to.be('/api/search/lookup'); - expect(requestOptions.params.m).to.be('cpu'); - }); - - it('tag_values(cpu, test) should generate lookup query', function() { - ctx.ds.metricFindQuery('tag_values(cpu, hostname)').then(function(data) { - results = data; - }); - ctx.$rootScope.$apply(); - expect(requestOptions.url).to.be('/api/search/lookup'); - expect(requestOptions.params.m).to.be('cpu{hostname=*}'); - }); - - it('tag_values(cpu, test) should generate lookup query', function() { - ctx.ds.metricFindQuery('tag_values(cpu, hostname, env=$env)').then(function(data) { - results = data; - }); - ctx.$rootScope.$apply(); - expect(requestOptions.url).to.be('/api/search/lookup'); - expect(requestOptions.params.m).to.be('cpu{hostname=*,env=$env}'); - }); - - it('tag_values(cpu, test) should generate lookup query', function() { - ctx.ds.metricFindQuery('tag_values(cpu, hostname, env=$env, region=$region)').then(function(data) { - results = data; - }); - ctx.$rootScope.$apply(); - expect(requestOptions.url).to.be('/api/search/lookup'); - expect(requestOptions.params.m).to.be('cpu{hostname=*,env=$env,region=$region}'); - }); - - it('suggest_tagk() should generate api suggest query', function() { - ctx.ds.metricFindQuery('suggest_tagk(foo)').then(function(data) { - results = data; - }); - ctx.$rootScope.$apply(); - expect(requestOptions.url).to.be('/api/suggest'); - expect(requestOptions.params.type).to.be('tagk'); - expect(requestOptions.params.q).to.be('foo'); - }); - - it('suggest_tagv() should generate api suggest query', function() { - ctx.ds.metricFindQuery('suggest_tagv(bar)').then(function(data) { - results = data; - }); - ctx.$rootScope.$apply(); - expect(requestOptions.url).to.be('/api/suggest'); - expect(requestOptions.params.type).to.be('tagv'); - expect(requestOptions.params.q).to.be('bar'); - }); - }); -}); diff --git a/public/app/plugins/datasource/opentsdb/specs/datasource.jest.ts b/public/app/plugins/datasource/opentsdb/specs/datasource.jest.ts new file mode 100644 index 00000000000..73eca7cffde --- /dev/null +++ b/public/app/plugins/datasource/opentsdb/specs/datasource.jest.ts @@ -0,0 +1,91 @@ +import OpenTsDatasource from '../datasource'; +import $q from 'q'; + +describe('opentsdb', () => { + let ctx = { + backendSrv: {}, + ds: {}, + templateSrv: { + replace: str => str, + }, + }; + let instanceSettings = { url: '', jsonData: { tsdbVersion: 1 } }; + + beforeEach(() => { + ctx.ctrl = new OpenTsDatasource(instanceSettings, $q, ctx.backendSrv, ctx.templateSrv); + }); + + describe('When performing metricFindQuery', () => { + var results; + var requestOptions; + + beforeEach(async () => { + ctx.backendSrv.datasourceRequest = await function(options) { + requestOptions = options; + return Promise.resolve({ + data: [{ target: 'prod1.count', datapoints: [[10, 1], [12, 1]] }], + }); + }; + }); + + it('metrics() should generate api suggest query', () => { + ctx.ctrl.metricFindQuery('metrics(pew)').then(function(data) { + results = data; + }); + expect(requestOptions.url).toBe('/api/suggest'); + expect(requestOptions.params.type).toBe('metrics'); + expect(requestOptions.params.q).toBe('pew'); + expect(results).not.toBe(null); + }); + + it('tag_names(cpu) should generate lookup query', () => { + ctx.ctrl.metricFindQuery('tag_names(cpu)').then(function(data) { + results = data; + }); + expect(requestOptions.url).toBe('/api/search/lookup'); + expect(requestOptions.params.m).toBe('cpu'); + }); + + it('tag_values(cpu, test) should generate lookup query', () => { + ctx.ctrl.metricFindQuery('tag_values(cpu, hostname)').then(function(data) { + results = data; + }); + expect(requestOptions.url).toBe('/api/search/lookup'); + expect(requestOptions.params.m).toBe('cpu{hostname=*}'); + }); + + it('tag_values(cpu, test) should generate lookup query', () => { + ctx.ctrl.metricFindQuery('tag_values(cpu, hostname, env=$env)').then(function(data) { + results = data; + }); + expect(requestOptions.url).toBe('/api/search/lookup'); + expect(requestOptions.params.m).toBe('cpu{hostname=*,env=$env}'); + }); + + it('tag_values(cpu, test) should generate lookup query', () => { + ctx.ctrl.metricFindQuery('tag_values(cpu, hostname, env=$env, region=$region)').then(function(data) { + results = data; + }); + expect(requestOptions.url).toBe('/api/search/lookup'); + expect(requestOptions.params.m).toBe('cpu{hostname=*,env=$env,region=$region}'); + }); + + it('suggest_tagk() should generate api suggest query', () => { + ctx.ctrl.metricFindQuery('suggest_tagk(foo)').then(function(data) { + results = data; + }); + expect(requestOptions.url).toBe('/api/suggest'); + expect(requestOptions.params.type).toBe('tagk'); + expect(requestOptions.params.q).toBe('foo'); + }); + + it('suggest_tagv() should generate api suggest query', () => { + ctx.ctrl.metricFindQuery('suggest_tagv(bar)').then(function(data) { + results = data; + }); + expect(requestOptions.url).toBe('/api/suggest'); + expect(requestOptions.params.type).toBe('tagv'); + expect(requestOptions.params.q).toBe('bar'); + }); + }); +}); From ccd964e1dfb2e48c0c38305653b314804fc4c6b2 Mon Sep 17 00:00:00 2001 From: Tobias Skarhed Date: Mon, 6 Aug 2018 10:57:58 +0200 Subject: [PATCH 301/786] Convert query control --- .../opentsdb/specs/query-ctrl-specs.ts | 113 ------------------ .../opentsdb/specs/query_ctrl.jest.ts | 93 ++++++++++++++ 2 files changed, 93 insertions(+), 113 deletions(-) delete mode 100644 public/app/plugins/datasource/opentsdb/specs/query-ctrl-specs.ts create mode 100644 public/app/plugins/datasource/opentsdb/specs/query_ctrl.jest.ts diff --git a/public/app/plugins/datasource/opentsdb/specs/query-ctrl-specs.ts b/public/app/plugins/datasource/opentsdb/specs/query-ctrl-specs.ts deleted file mode 100644 index 97fc11e9d2f..00000000000 --- a/public/app/plugins/datasource/opentsdb/specs/query-ctrl-specs.ts +++ /dev/null @@ -1,113 +0,0 @@ -import { describe, beforeEach, it, sinon, expect, angularMocks } from 'test/lib/common'; -import helpers from 'test/specs/helpers'; -import { OpenTsQueryCtrl } from '../query_ctrl'; - -describe('OpenTsQueryCtrl', function() { - var ctx = new helpers.ControllerTestContext(); - - beforeEach(angularMocks.module('grafana.core')); - beforeEach(angularMocks.module('grafana.services')); - beforeEach( - angularMocks.module(function($compileProvider) { - $compileProvider.preAssignBindingsEnabled(true); - }) - ); - - beforeEach(ctx.providePhase(['backendSrv', 'templateSrv'])); - - beforeEach(ctx.providePhase()); - beforeEach( - angularMocks.inject(($rootScope, $controller, $q) => { - ctx.$q = $q; - ctx.scope = $rootScope.$new(); - ctx.target = { target: '' }; - ctx.panelCtrl = { - panel: { - targets: [ctx.target], - }, - }; - ctx.panelCtrl.refresh = sinon.spy(); - ctx.datasource.getAggregators = sinon.stub().returns(ctx.$q.when([])); - ctx.datasource.getFilterTypes = sinon.stub().returns(ctx.$q.when([])); - - ctx.ctrl = $controller( - OpenTsQueryCtrl, - { $scope: ctx.scope }, - { - panelCtrl: ctx.panelCtrl, - datasource: ctx.datasource, - target: ctx.target, - } - ); - ctx.scope.$digest(); - }) - ); - - describe('init query_ctrl variables', function() { - it('filter types should be initialized', function() { - expect(ctx.ctrl.filterTypes.length).to.be(7); - }); - - it('aggregators should be initialized', function() { - expect(ctx.ctrl.aggregators.length).to.be(8); - }); - - it('fill policy options should be initialized', function() { - expect(ctx.ctrl.fillPolicies.length).to.be(4); - }); - }); - - describe('when adding filters and tags', function() { - it('addTagMode should be false when closed', function() { - ctx.ctrl.addTagMode = true; - ctx.ctrl.closeAddTagMode(); - expect(ctx.ctrl.addTagMode).to.be(false); - }); - - it('addFilterMode should be false when closed', function() { - ctx.ctrl.addFilterMode = true; - ctx.ctrl.closeAddFilterMode(); - expect(ctx.ctrl.addFilterMode).to.be(false); - }); - - it('removing a tag from the tags list', function() { - ctx.ctrl.target.tags = { tagk: 'tag_key', tagk2: 'tag_value2' }; - ctx.ctrl.removeTag('tagk'); - expect(Object.keys(ctx.ctrl.target.tags).length).to.be(1); - }); - - it('removing a filter from the filters list', function() { - ctx.ctrl.target.filters = [ - { - tagk: 'tag_key', - filter: 'tag_value2', - type: 'wildcard', - groupBy: true, - }, - ]; - ctx.ctrl.removeFilter(0); - expect(ctx.ctrl.target.filters.length).to.be(0); - }); - - it('adding a filter when tags exist should generate error', function() { - ctx.ctrl.target.tags = { tagk: 'tag_key', tagk2: 'tag_value2' }; - ctx.ctrl.addFilter(); - expect(ctx.ctrl.errors.filters).to.be( - 'Please remove tags to use filters, tags and filters are mutually exclusive.' - ); - }); - - it('adding a tag when filters exist should generate error', function() { - ctx.ctrl.target.filters = [ - { - tagk: 'tag_key', - filter: 'tag_value2', - type: 'wildcard', - groupBy: true, - }, - ]; - ctx.ctrl.addTag(); - expect(ctx.ctrl.errors.tags).to.be('Please remove filters to use tags, tags and filters are mutually exclusive.'); - }); - }); -}); diff --git a/public/app/plugins/datasource/opentsdb/specs/query_ctrl.jest.ts b/public/app/plugins/datasource/opentsdb/specs/query_ctrl.jest.ts new file mode 100644 index 00000000000..58a10b21207 --- /dev/null +++ b/public/app/plugins/datasource/opentsdb/specs/query_ctrl.jest.ts @@ -0,0 +1,93 @@ +import { OpenTsQueryCtrl } from '../query_ctrl'; + +describe('OpenTsQueryCtrl', () => { + var ctx = { + target: { target: '' }, + datasource: { + tsdbVersion: '', + getAggregators: () => Promise.resolve([]), + getFilterTypes: () => Promise.resolve([]), + }, + }; + + ctx.panelCtrl = { + panel: { + targets: [ctx.target], + }, + refresh: () => {}, + }; + + OpenTsQueryCtrl.prototype = Object.assign(OpenTsQueryCtrl.prototype, ctx); + + beforeEach(() => { + ctx.ctrl = new OpenTsQueryCtrl({}, {}); + }); + + describe('init query_ctrl variables', () => { + it('filter types should be initialized', () => { + expect(ctx.ctrl.filterTypes.length).toBe(7); + }); + + it('aggregators should be initialized', () => { + expect(ctx.ctrl.aggregators.length).toBe(8); + }); + + it('fill policy options should be initialized', () => { + expect(ctx.ctrl.fillPolicies.length).toBe(4); + }); + }); + + describe('when adding filters and tags', () => { + it('addTagMode should be false when closed', () => { + ctx.ctrl.addTagMode = true; + ctx.ctrl.closeAddTagMode(); + expect(ctx.ctrl.addTagMode).toBe(false); + }); + + it('addFilterMode should be false when closed', () => { + ctx.ctrl.addFilterMode = true; + ctx.ctrl.closeAddFilterMode(); + expect(ctx.ctrl.addFilterMode).toBe(false); + }); + + it('removing a tag from the tags list', () => { + ctx.ctrl.target.tags = { tagk: 'tag_key', tagk2: 'tag_value2' }; + ctx.ctrl.removeTag('tagk'); + expect(Object.keys(ctx.ctrl.target.tags).length).toBe(1); + }); + + it('removing a filter from the filters list', () => { + ctx.ctrl.target.filters = [ + { + tagk: 'tag_key', + filter: 'tag_value2', + type: 'wildcard', + groupBy: true, + }, + ]; + ctx.ctrl.removeFilter(0); + expect(ctx.ctrl.target.filters.length).toBe(0); + }); + + it('adding a filter when tags exist should generate error', () => { + ctx.ctrl.target.tags = { tagk: 'tag_key', tagk2: 'tag_value2' }; + ctx.ctrl.addFilter(); + expect(ctx.ctrl.errors.filters).toBe( + 'Please remove tags to use filters, tags and filters are mutually exclusive.' + ); + }); + + it('adding a tag when filters exist should generate error', () => { + ctx.ctrl.target.filters = [ + { + tagk: 'tag_key', + filter: 'tag_value2', + type: 'wildcard', + groupBy: true, + }, + ]; + ctx.ctrl.addTag(); + expect(ctx.ctrl.errors.tags).toBe('Please remove filters to use tags, tags and filters are mutually exclusive.'); + }); + }); +}); From 7f4723a9a7402621c6ac02ea66622db7eb2829b6 Mon Sep 17 00:00:00 2001 From: Tobias Skarhed Date: Wed, 1 Aug 2018 14:21:05 +0200 Subject: [PATCH 302/786] Begin conversion --- .../templating/specs/variable_srv.jest.ts | 595 ++++++++++++++++++ 1 file changed, 595 insertions(+) create mode 100644 public/app/features/templating/specs/variable_srv.jest.ts diff --git a/public/app/features/templating/specs/variable_srv.jest.ts b/public/app/features/templating/specs/variable_srv.jest.ts new file mode 100644 index 00000000000..d38a89675e6 --- /dev/null +++ b/public/app/features/templating/specs/variable_srv.jest.ts @@ -0,0 +1,595 @@ +import '../all'; +import { VariableSrv } from '../variable_srv'; +import moment from 'moment'; +import $q from 'q'; +// import { model } from 'mobx-state-tree/dist/internal'; +// import { Emitter } from 'app/core/core'; + +describe('VariableSrv', function() { + var ctx = { + datasourceSrv: {}, + timeSrv: { + timeRange: () => {}, + }, + $rootScope: { + $on: () => {}, + }, + $injector: { + instantiate: (ctr, obj) => new ctr(obj.model), + }, + templateSrv: { + setGrafanaVariable: jest.fn(), + init: () => {}, + updateTemplateData: () => {}, + }, + $location: { + search: () => {}, + }, + }; + + // beforeEach(ctx.providePhase(['datasourceSrv', 'timeSrv', 'templateSrv', '$location'])); + // beforeEach( + // angularMocks.inject(($rootScope, $q, $location, $injector) => { + // ctx.$q = $q; + // ctx.$rootScope = $rootScope; + // ctx.$location = $location; + // ctx.variableSrv = $injector.get('variableSrv'); + // ctx.variableSrv.init({ + // templating: { list: [] }, + // events: new Emitter(), + // updateSubmenuVisibility: sinon.stub(), + // }); + // ctx.$rootScope.$digest(); + // }) + // ); + + function describeUpdateVariable(desc, fn) { + describe(desc, function() { + var scenario: any = {}; + scenario.setup = function(setupFn) { + scenario.setupFn = setupFn; + }; + + beforeEach(function() { + scenario.setupFn(); + + var ds: any = {}; + ds.metricFindQuery = Promise.resolve(scenario.queryResult); + + ctx.variableSrv = new VariableSrv(ctx.$rootScope, $q, ctx.$location, ctx.$injector, ctx.templateSrv); + + ctx.variableSrv.timeSrv = ctx.timeSrv; + console.log(ctx.variableSrv.timeSrv); + ctx.variableSrv.datasourceSrv = { + get: Promise.resolve(ds), + getMetricSources: () => scenario.metricSources, + }; + + ctx.variableSrv.init({ + templating: { list: [] }, + updateSubmenuVisibility: () => {}, + }); + + scenario.variable = ctx.variableSrv.createVariableFromModel(scenario.variableModel); + ctx.variableSrv.addVariable(scenario.variable); + + ctx.variableSrv.updateOptions(scenario.variable); + // ctx.$rootScope.$digest(); + }); + + fn(scenario); + }); + } + + describeUpdateVariable('interval variable without auto', scenario => { + scenario.setup(() => { + scenario.variableModel = { + type: 'interval', + query: '1s,2h,5h,1d', + name: 'test', + }; + }); + + it('should update options array', () => { + expect(scenario.variable.options.length).toBe(4); + expect(scenario.variable.options[0].text).toBe('1s'); + expect(scenario.variable.options[0].value).toBe('1s'); + }); + }); + + // + // Interval variable update + // + describeUpdateVariable('interval variable with auto', scenario => { + scenario.setup(() => { + scenario.variableModel = { + type: 'interval', + query: '1s,2h,5h,1d', + name: 'test', + auto: true, + auto_count: 10, + }; + + var range = { + from: moment(new Date()) + .subtract(7, 'days') + .toDate(), + to: new Date(), + }; + + ctx.timeSrv.timeRange = () => range; + // ctx.templateSrv.setGrafanaVariable = jest.fn(); + }); + + it('should update options array', function() { + expect(scenario.variable.options.length).toBe(5); + expect(scenario.variable.options[0].text).toBe('auto'); + expect(scenario.variable.options[0].value).toBe('$__auto_interval_test'); + }); + + it('should set $__auto_interval_test', function() { + var call = ctx.templateSrv.setGrafanaVariable.firstCall; + expect(call.args[0]).toBe('$__auto_interval_test'); + expect(call.args[1]).toBe('12h'); + }); + + // updateAutoValue() gets called twice: once directly once via VariableSrv.validateVariableSelectionState() + // So use lastCall instead of a specific call number + it('should set $__auto_interval', function() { + var call = ctx.templateSrv.setGrafanaVariable.lastCall; + expect(call.args[0]).toBe('$__auto_interval'); + expect(call.args[1]).toBe('12h'); + }); + }); + + // + // Query variable update + // + describeUpdateVariable('query variable with empty current object and refresh', function(scenario) { + scenario.setup(function() { + scenario.variableModel = { + type: 'query', + query: '', + name: 'test', + current: {}, + }; + scenario.queryResult = [{ text: 'backend1' }, { text: 'backend2' }]; + }); + + it('should set current value to first option', function() { + expect(scenario.variable.options.length).toBe(2); + expect(scenario.variable.current.value).toBe('backend1'); + }); + }); + + describeUpdateVariable( + 'query variable with multi select and new options does not contain some selected values', + function(scenario) { + scenario.setup(function() { + scenario.variableModel = { + type: 'query', + query: '', + name: 'test', + current: { + value: ['val1', 'val2', 'val3'], + text: 'val1 + val2 + val3', + }, + }; + scenario.queryResult = [{ text: 'val2' }, { text: 'val3' }]; + }); + + it('should update current value', function() { + expect(scenario.variable.current.value).toEqual(['val2', 'val3']); + expect(scenario.variable.current.text).toEqual('val2 + val3'); + }); + } + ); + + describeUpdateVariable( + 'query variable with multi select and new options does not contain any selected values', + function(scenario) { + scenario.setup(function() { + scenario.variableModel = { + type: 'query', + query: '', + name: 'test', + current: { + value: ['val1', 'val2', 'val3'], + text: 'val1 + val2 + val3', + }, + }; + scenario.queryResult = [{ text: 'val5' }, { text: 'val6' }]; + }); + + it('should update current value with first one', function() { + expect(scenario.variable.current.value).toEqual('val5'); + expect(scenario.variable.current.text).toEqual('val5'); + }); + } + ); + + describeUpdateVariable('query variable with multi select and $__all selected', function(scenario) { + scenario.setup(function() { + scenario.variableModel = { + type: 'query', + query: '', + name: 'test', + includeAll: true, + current: { + value: ['$__all'], + text: 'All', + }, + }; + scenario.queryResult = [{ text: 'val5' }, { text: 'val6' }]; + }); + + it('should keep current All value', function() { + expect(scenario.variable.current.value).toEqual(['$__all']); + expect(scenario.variable.current.text).toEqual('All'); + }); + }); + + describeUpdateVariable('query variable with numeric results', function(scenario) { + scenario.setup(function() { + scenario.variableModel = { + type: 'query', + query: '', + name: 'test', + current: {}, + }; + scenario.queryResult = [{ text: 12, value: 12 }]; + }); + + it('should set current value to first option', function() { + expect(scenario.variable.current.value).toBe('12'); + expect(scenario.variable.options[0].value).toBe('12'); + expect(scenario.variable.options[0].text).toBe('12'); + }); + }); + + describeUpdateVariable('basic query variable', function(scenario) { + scenario.setup(function() { + scenario.variableModel = { type: 'query', query: 'apps.*', name: 'test' }; + scenario.queryResult = [{ text: 'backend1' }, { text: 'backend2' }]; + }); + + it('should update options array', function() { + expect(scenario.variable.options.length).toBe(2); + expect(scenario.variable.options[0].text).toBe('backend1'); + expect(scenario.variable.options[0].value).toBe('backend1'); + expect(scenario.variable.options[1].value).toBe('backend2'); + }); + + it('should select first option as value', function() { + expect(scenario.variable.current.value).toBe('backend1'); + }); + }); + + describeUpdateVariable('and existing value still exists in options', function(scenario) { + scenario.setup(function() { + scenario.variableModel = { type: 'query', query: 'apps.*', name: 'test' }; + scenario.variableModel.current = { value: 'backend2', text: 'backend2' }; + scenario.queryResult = [{ text: 'backend1' }, { text: 'backend2' }]; + }); + + it('should keep variable value', function() { + expect(scenario.variable.current.text).toBe('backend2'); + }); + }); + + describeUpdateVariable('and regex pattern exists', function(scenario) { + scenario.setup(function() { + scenario.variableModel = { type: 'query', query: 'apps.*', name: 'test' }; + scenario.variableModel.regex = '/apps.*(backend_[0-9]+)/'; + scenario.queryResult = [ + { text: 'apps.backend.backend_01.counters.req' }, + { text: 'apps.backend.backend_02.counters.req' }, + ]; + }); + + it('should extract and use match group', function() { + expect(scenario.variable.options[0].value).toBe('backend_01'); + }); + }); + + describeUpdateVariable('and regex pattern exists and no match', function(scenario) { + scenario.setup(function() { + scenario.variableModel = { type: 'query', query: 'apps.*', name: 'test' }; + scenario.variableModel.regex = '/apps.*(backendasd[0-9]+)/'; + scenario.queryResult = [ + { text: 'apps.backend.backend_01.counters.req' }, + { text: 'apps.backend.backend_02.counters.req' }, + ]; + }); + + it('should not add non matching items, None option should be added instead', function() { + expect(scenario.variable.options.length).toBe(1); + expect(scenario.variable.options[0].isNone).toBe(true); + }); + }); + + describeUpdateVariable('regex pattern without slashes', function(scenario) { + scenario.setup(function() { + scenario.variableModel = { type: 'query', query: 'apps.*', name: 'test' }; + scenario.variableModel.regex = 'backend_01'; + scenario.queryResult = [ + { text: 'apps.backend.backend_01.counters.req' }, + { text: 'apps.backend.backend_02.counters.req' }, + ]; + }); + + it('should return matches options', function() { + expect(scenario.variable.options.length).toBe(1); + }); + }); + + describeUpdateVariable('regex pattern remove duplicates', function(scenario) { + scenario.setup(function() { + scenario.variableModel = { type: 'query', query: 'apps.*', name: 'test' }; + scenario.variableModel.regex = '/backend_01/'; + scenario.queryResult = [ + { text: 'apps.backend.backend_01.counters.req' }, + { text: 'apps.backend.backend_01.counters.req' }, + ]; + }); + + it('should return matches options', function() { + expect(scenario.variable.options.length).toBe(1); + }); + }); + + describeUpdateVariable('with include All', function(scenario) { + scenario.setup(function() { + scenario.variableModel = { + type: 'query', + query: 'apps.*', + name: 'test', + includeAll: true, + }; + scenario.queryResult = [{ text: 'backend1' }, { text: 'backend2' }, { text: 'backend3' }]; + }); + + it('should add All option', function() { + expect(scenario.variable.options[0].text).toBe('All'); + expect(scenario.variable.options[0].value).toBe('$__all'); + }); + }); + + describeUpdateVariable('with include all and custom value', function(scenario) { + scenario.setup(function() { + scenario.variableModel = { + type: 'query', + query: 'apps.*', + name: 'test', + includeAll: true, + allValue: '*', + }; + scenario.queryResult = [{ text: 'backend1' }, { text: 'backend2' }, { text: 'backend3' }]; + }); + + it('should add All option with custom value', function() { + expect(scenario.variable.options[0].value).toBe('$__all'); + }); + }); + + describeUpdateVariable('without sort', function(scenario) { + scenario.setup(function() { + scenario.variableModel = { + type: 'query', + query: 'apps.*', + name: 'test', + sort: 0, + }; + scenario.queryResult = [{ text: 'bbb2' }, { text: 'aaa10' }, { text: 'ccc3' }]; + }); + + it('should return options without sort', function() { + expect(scenario.variable.options[0].text).toBe('bbb2'); + expect(scenario.variable.options[1].text).toBe('aaa10'); + expect(scenario.variable.options[2].text).toBe('ccc3'); + }); + }); + + describeUpdateVariable('with alphabetical sort (asc)', function(scenario) { + scenario.setup(function() { + scenario.variableModel = { + type: 'query', + query: 'apps.*', + name: 'test', + sort: 1, + }; + scenario.queryResult = [{ text: 'bbb2' }, { text: 'aaa10' }, { text: 'ccc3' }]; + }); + + it('should return options with alphabetical sort', function() { + expect(scenario.variable.options[0].text).toBe('aaa10'); + expect(scenario.variable.options[1].text).toBe('bbb2'); + expect(scenario.variable.options[2].text).toBe('ccc3'); + }); + }); + + describeUpdateVariable('with alphabetical sort (desc)', function(scenario) { + scenario.setup(function() { + scenario.variableModel = { + type: 'query', + query: 'apps.*', + name: 'test', + sort: 2, + }; + scenario.queryResult = [{ text: 'bbb2' }, { text: 'aaa10' }, { text: 'ccc3' }]; + }); + + it('should return options with alphabetical sort', function() { + expect(scenario.variable.options[0].text).toBe('ccc3'); + expect(scenario.variable.options[1].text).toBe('bbb2'); + expect(scenario.variable.options[2].text).toBe('aaa10'); + }); + }); + + describeUpdateVariable('with numerical sort (asc)', function(scenario) { + scenario.setup(function() { + scenario.variableModel = { + type: 'query', + query: 'apps.*', + name: 'test', + sort: 3, + }; + scenario.queryResult = [{ text: 'bbb2' }, { text: 'aaa10' }, { text: 'ccc3' }]; + }); + + it('should return options with numerical sort', function() { + expect(scenario.variable.options[0].text).toBe('bbb2'); + expect(scenario.variable.options[1].text).toBe('ccc3'); + expect(scenario.variable.options[2].text).toBe('aaa10'); + }); + }); + + describeUpdateVariable('with numerical sort (desc)', function(scenario) { + scenario.setup(function() { + scenario.variableModel = { + type: 'query', + query: 'apps.*', + name: 'test', + sort: 4, + }; + scenario.queryResult = [{ text: 'bbb2' }, { text: 'aaa10' }, { text: 'ccc3' }]; + }); + + it('should return options with numerical sort', function() { + expect(scenario.variable.options[0].text).toBe('aaa10'); + expect(scenario.variable.options[1].text).toBe('ccc3'); + expect(scenario.variable.options[2].text).toBe('bbb2'); + }); + }); + + // + // datasource variable update + // + describeUpdateVariable('datasource variable with regex filter', function(scenario) { + scenario.setup(function() { + scenario.variableModel = { + type: 'datasource', + query: 'graphite', + name: 'test', + current: { value: 'backend4_pee', text: 'backend4_pee' }, + regex: '/pee$/', + }; + scenario.metricSources = [ + { name: 'backend1', meta: { id: 'influx' } }, + { name: 'backend2_pee', meta: { id: 'graphite' } }, + { name: 'backend3', meta: { id: 'graphite' } }, + { name: 'backend4_pee', meta: { id: 'graphite' } }, + ]; + }); + + it('should set only contain graphite ds and filtered using regex', function() { + expect(scenario.variable.options.length).toBe(2); + expect(scenario.variable.options[0].value).toBe('backend2_pee'); + expect(scenario.variable.options[1].value).toBe('backend4_pee'); + }); + + it('should keep current value if available', function() { + expect(scenario.variable.current.value).toBe('backend4_pee'); + }); + }); + + // + // Custom variable update + // + describeUpdateVariable('update custom variable', function(scenario) { + scenario.setup(function() { + scenario.variableModel = { + type: 'custom', + query: 'hej, hop, asd', + name: 'test', + }; + }); + + it('should update options array', function() { + expect(scenario.variable.options.length).toBe(3); + expect(scenario.variable.options[0].text).toBe('hej'); + expect(scenario.variable.options[1].value).toBe('hop'); + }); + }); + + describe('multiple interval variables with auto', function() { + var variable1, variable2; + + beforeEach(function() { + var range = { + from: moment(new Date()) + .subtract(7, 'days') + .toDate(), + to: new Date(), + }; + ctx.timeSrv.timeRange = () => range; + ctx.templateSrv.setGrafanaVariable = jest.fn(); + + var variableModel1 = { + type: 'interval', + query: '1s,2h,5h,1d', + name: 'variable1', + auto: true, + auto_count: 10, + }; + variable1 = ctx.variableSrv.createVariableFromModel(variableModel1); + ctx.variableSrv.addVariable(variable1); + + var variableModel2 = { + type: 'interval', + query: '1s,2h,5h', + name: 'variable2', + auto: true, + auto_count: 1000, + }; + variable2 = ctx.variableSrv.createVariableFromModel(variableModel2); + ctx.variableSrv.addVariable(variable2); + + ctx.variableSrv.updateOptions(variable1); + ctx.variableSrv.updateOptions(variable2); + ctx.$rootScope.$digest(); + }); + + it('should update options array', function() { + expect(variable1.options.length).toBe(5); + expect(variable1.options[0].text).toBe('auto'); + expect(variable1.options[0].value).toBe('$__auto_interval_variable1'); + expect(variable2.options.length).toBe(4); + expect(variable2.options[0].text).toBe('auto'); + expect(variable2.options[0].value).toBe('$__auto_interval_variable2'); + }); + + it('should correctly set $__auto_interval_variableX', function() { + var variable1Set, + variable2Set, + legacySet, + unknownSet = false; + // updateAutoValue() gets called repeatedly: once directly once via VariableSrv.validateVariableSelectionState() + // So check that all calls are valid rather than expect a specific number and/or ordering of calls + for (var i = 0; i < ctx.templateSrv.setGrafanaVariable.mock.calls.length; i++) { + var call = ctx.templateSrv.setGrafanaVariable.mock.calls[i]; + switch (call.args[0]) { + case '$__auto_interval_variable1': + expect(call[1]).toBe('12h'); + variable1Set = true; + break; + case '$__auto_interval_variable2': + expect(call[1]).toBe('10m'); + variable2Set = true; + break; + case '$__auto_interval': + expect(call[1]).toEqual(expect.stringMatching(/^(12h|10m)$/)); + legacySet = true; + break; + default: + unknownSet = true; + break; + } + } + expect(variable1Set).toBe.equal(true); + expect(variable2Set).toBe.equal(true); + expect(legacySet).toBe.equal(true); + expect(unknownSet).toBe.equal(false); + }); + }); +}); From 034ca6961026c828960a7397806fe8cb1d91cdb4 Mon Sep 17 00:00:00 2001 From: Tobias Skarhed Date: Wed, 1 Aug 2018 15:17:10 +0200 Subject: [PATCH 303/786] Add mock constructor --- .../templating/specs/variable_srv.jest.ts | 59 +++++++++++++------ 1 file changed, 41 insertions(+), 18 deletions(-) diff --git a/public/app/features/templating/specs/variable_srv.jest.ts b/public/app/features/templating/specs/variable_srv.jest.ts index d38a89675e6..33a28cda08e 100644 --- a/public/app/features/templating/specs/variable_srv.jest.ts +++ b/public/app/features/templating/specs/variable_srv.jest.ts @@ -19,8 +19,14 @@ describe('VariableSrv', function() { }, templateSrv: { setGrafanaVariable: jest.fn(), - init: () => {}, + init: vars => { + this.variables = vars; + }, updateTemplateData: () => {}, + replace: str => + str.replace(this.regex, match => { + return match; + }), }, $location: { search: () => {}, @@ -54,17 +60,20 @@ describe('VariableSrv', function() { scenario.setupFn(); var ds: any = {}; - ds.metricFindQuery = Promise.resolve(scenario.queryResult); + ds.metricFindQuery = () => Promise.resolve(scenario.queryResult); ctx.variableSrv = new VariableSrv(ctx.$rootScope, $q, ctx.$location, ctx.$injector, ctx.templateSrv); ctx.variableSrv.timeSrv = ctx.timeSrv; - console.log(ctx.variableSrv.timeSrv); - ctx.variableSrv.datasourceSrv = { - get: Promise.resolve(ds), + ctx.datasourceSrv = { + get: () => Promise.resolve(ds), getMetricSources: () => scenario.metricSources, }; + ctx.$injector.instantiate = (ctr, model) => { + return getVarMockConstructor(ctr, model, ctx); + }; + ctx.variableSrv.init({ templating: { list: [] }, updateSubmenuVisibility: () => {}, @@ -74,7 +83,6 @@ describe('VariableSrv', function() { ctx.variableSrv.addVariable(scenario.variable); ctx.variableSrv.updateOptions(scenario.variable); - // ctx.$rootScope.$digest(); }); fn(scenario); @@ -128,17 +136,17 @@ describe('VariableSrv', function() { }); it('should set $__auto_interval_test', function() { - var call = ctx.templateSrv.setGrafanaVariable.firstCall; - expect(call.args[0]).toBe('$__auto_interval_test'); - expect(call.args[1]).toBe('12h'); + var call = ctx.templateSrv.setGrafanaVariable.mock.calls[0]; + expect(call[0]).toBe('$__auto_interval_test'); + expect(call[1]).toBe('12h'); }); // updateAutoValue() gets called twice: once directly once via VariableSrv.validateVariableSelectionState() // So use lastCall instead of a specific call number it('should set $__auto_interval', function() { - var call = ctx.templateSrv.setGrafanaVariable.lastCall; - expect(call.args[0]).toBe('$__auto_interval'); - expect(call.args[1]).toBe('12h'); + var call = ctx.templateSrv.setGrafanaVariable.mock.calls.pop(); + expect(call[0]).toBe('$__auto_interval'); + expect(call[1]).toBe('12h'); }); }); @@ -547,7 +555,7 @@ describe('VariableSrv', function() { ctx.variableSrv.updateOptions(variable1); ctx.variableSrv.updateOptions(variable2); - ctx.$rootScope.$digest(); + // ctx.$rootScope.$digest(); }); it('should update options array', function() { @@ -568,7 +576,7 @@ describe('VariableSrv', function() { // So check that all calls are valid rather than expect a specific number and/or ordering of calls for (var i = 0; i < ctx.templateSrv.setGrafanaVariable.mock.calls.length; i++) { var call = ctx.templateSrv.setGrafanaVariable.mock.calls[i]; - switch (call.args[0]) { + switch (call[0]) { case '$__auto_interval_variable1': expect(call[1]).toBe('12h'); variable1Set = true; @@ -586,10 +594,25 @@ describe('VariableSrv', function() { break; } } - expect(variable1Set).toBe.equal(true); - expect(variable2Set).toBe.equal(true); - expect(legacySet).toBe.equal(true); - expect(unknownSet).toBe.equal(false); + expect(variable1Set).toEqual(true); + expect(variable2Set).toEqual(true); + expect(legacySet).toEqual(true); + expect(unknownSet).toEqual(false); }); }); }); + +function getVarMockConstructor(variable, model, ctx) { + switch (model.model.type) { + case 'datasource': + return new variable(model.model, ctx.datasourceSrv, ctx.variableSrv, ctx.templateSrv); + case 'query': + return new variable(model.model, ctx.datasourceSrv, ctx.templateSrv, ctx.variableSrv); + case 'interval': + return new variable(model.model, ctx.timeSrv, ctx.templateSrv, ctx.variableSrv); + case 'custom': + return new variable(model.model, ctx.variableSrv); + default: + return new variable(model.model); + } +} From 46dd4eba9e9a1777bdba24b1fb4089bcec601816 Mon Sep 17 00:00:00 2001 From: Tobias Skarhed Date: Wed, 1 Aug 2018 15:38:48 +0200 Subject: [PATCH 304/786] All tests passing --- .../templating/specs/variable_srv.jest.ts | 122 ++++++++---------- 1 file changed, 52 insertions(+), 70 deletions(-) diff --git a/public/app/features/templating/specs/variable_srv.jest.ts b/public/app/features/templating/specs/variable_srv.jest.ts index 33a28cda08e..f7796434b5e 100644 --- a/public/app/features/templating/specs/variable_srv.jest.ts +++ b/public/app/features/templating/specs/variable_srv.jest.ts @@ -2,8 +2,6 @@ import '../all'; import { VariableSrv } from '../variable_srv'; import moment from 'moment'; import $q from 'q'; -// import { model } from 'mobx-state-tree/dist/internal'; -// import { Emitter } from 'app/core/core'; describe('VariableSrv', function() { var ctx = { @@ -33,30 +31,14 @@ describe('VariableSrv', function() { }, }; - // beforeEach(ctx.providePhase(['datasourceSrv', 'timeSrv', 'templateSrv', '$location'])); - // beforeEach( - // angularMocks.inject(($rootScope, $q, $location, $injector) => { - // ctx.$q = $q; - // ctx.$rootScope = $rootScope; - // ctx.$location = $location; - // ctx.variableSrv = $injector.get('variableSrv'); - // ctx.variableSrv.init({ - // templating: { list: [] }, - // events: new Emitter(), - // updateSubmenuVisibility: sinon.stub(), - // }); - // ctx.$rootScope.$digest(); - // }) - // ); - function describeUpdateVariable(desc, fn) { - describe(desc, function() { + describe(desc, () => { var scenario: any = {}; scenario.setup = function(setupFn) { scenario.setupFn = setupFn; }; - beforeEach(function() { + beforeEach(async () => { scenario.setupFn(); var ds: any = {}; @@ -82,7 +64,7 @@ describe('VariableSrv', function() { scenario.variable = ctx.variableSrv.createVariableFromModel(scenario.variableModel); ctx.variableSrv.addVariable(scenario.variable); - ctx.variableSrv.updateOptions(scenario.variable); + await ctx.variableSrv.updateOptions(scenario.variable); }); fn(scenario); @@ -129,13 +111,13 @@ describe('VariableSrv', function() { // ctx.templateSrv.setGrafanaVariable = jest.fn(); }); - it('should update options array', function() { + it('should update options array', () => { expect(scenario.variable.options.length).toBe(5); expect(scenario.variable.options[0].text).toBe('auto'); expect(scenario.variable.options[0].value).toBe('$__auto_interval_test'); }); - it('should set $__auto_interval_test', function() { + it('should set $__auto_interval_test', () => { var call = ctx.templateSrv.setGrafanaVariable.mock.calls[0]; expect(call[0]).toBe('$__auto_interval_test'); expect(call[1]).toBe('12h'); @@ -143,7 +125,7 @@ describe('VariableSrv', function() { // updateAutoValue() gets called twice: once directly once via VariableSrv.validateVariableSelectionState() // So use lastCall instead of a specific call number - it('should set $__auto_interval', function() { + it('should set $__auto_interval', () => { var call = ctx.templateSrv.setGrafanaVariable.mock.calls.pop(); expect(call[0]).toBe('$__auto_interval'); expect(call[1]).toBe('12h'); @@ -154,7 +136,7 @@ describe('VariableSrv', function() { // Query variable update // describeUpdateVariable('query variable with empty current object and refresh', function(scenario) { - scenario.setup(function() { + scenario.setup(() => { scenario.variableModel = { type: 'query', query: '', @@ -164,7 +146,7 @@ describe('VariableSrv', function() { scenario.queryResult = [{ text: 'backend1' }, { text: 'backend2' }]; }); - it('should set current value to first option', function() { + it('should set current value to first option', () => { expect(scenario.variable.options.length).toBe(2); expect(scenario.variable.current.value).toBe('backend1'); }); @@ -173,7 +155,7 @@ describe('VariableSrv', function() { describeUpdateVariable( 'query variable with multi select and new options does not contain some selected values', function(scenario) { - scenario.setup(function() { + scenario.setup(() => { scenario.variableModel = { type: 'query', query: '', @@ -186,7 +168,7 @@ describe('VariableSrv', function() { scenario.queryResult = [{ text: 'val2' }, { text: 'val3' }]; }); - it('should update current value', function() { + it('should update current value', () => { expect(scenario.variable.current.value).toEqual(['val2', 'val3']); expect(scenario.variable.current.text).toEqual('val2 + val3'); }); @@ -196,7 +178,7 @@ describe('VariableSrv', function() { describeUpdateVariable( 'query variable with multi select and new options does not contain any selected values', function(scenario) { - scenario.setup(function() { + scenario.setup(() => { scenario.variableModel = { type: 'query', query: '', @@ -209,7 +191,7 @@ describe('VariableSrv', function() { scenario.queryResult = [{ text: 'val5' }, { text: 'val6' }]; }); - it('should update current value with first one', function() { + it('should update current value with first one', () => { expect(scenario.variable.current.value).toEqual('val5'); expect(scenario.variable.current.text).toEqual('val5'); }); @@ -217,7 +199,7 @@ describe('VariableSrv', function() { ); describeUpdateVariable('query variable with multi select and $__all selected', function(scenario) { - scenario.setup(function() { + scenario.setup(() => { scenario.variableModel = { type: 'query', query: '', @@ -231,14 +213,14 @@ describe('VariableSrv', function() { scenario.queryResult = [{ text: 'val5' }, { text: 'val6' }]; }); - it('should keep current All value', function() { + it('should keep current All value', () => { expect(scenario.variable.current.value).toEqual(['$__all']); expect(scenario.variable.current.text).toEqual('All'); }); }); describeUpdateVariable('query variable with numeric results', function(scenario) { - scenario.setup(function() { + scenario.setup(() => { scenario.variableModel = { type: 'query', query: '', @@ -248,7 +230,7 @@ describe('VariableSrv', function() { scenario.queryResult = [{ text: 12, value: 12 }]; }); - it('should set current value to first option', function() { + it('should set current value to first option', () => { expect(scenario.variable.current.value).toBe('12'); expect(scenario.variable.options[0].value).toBe('12'); expect(scenario.variable.options[0].text).toBe('12'); @@ -256,37 +238,37 @@ describe('VariableSrv', function() { }); describeUpdateVariable('basic query variable', function(scenario) { - scenario.setup(function() { + scenario.setup(() => { scenario.variableModel = { type: 'query', query: 'apps.*', name: 'test' }; scenario.queryResult = [{ text: 'backend1' }, { text: 'backend2' }]; }); - it('should update options array', function() { + it('should update options array', () => { expect(scenario.variable.options.length).toBe(2); expect(scenario.variable.options[0].text).toBe('backend1'); expect(scenario.variable.options[0].value).toBe('backend1'); expect(scenario.variable.options[1].value).toBe('backend2'); }); - it('should select first option as value', function() { + it('should select first option as value', () => { expect(scenario.variable.current.value).toBe('backend1'); }); }); describeUpdateVariable('and existing value still exists in options', function(scenario) { - scenario.setup(function() { + scenario.setup(() => { scenario.variableModel = { type: 'query', query: 'apps.*', name: 'test' }; scenario.variableModel.current = { value: 'backend2', text: 'backend2' }; scenario.queryResult = [{ text: 'backend1' }, { text: 'backend2' }]; }); - it('should keep variable value', function() { + it('should keep variable value', () => { expect(scenario.variable.current.text).toBe('backend2'); }); }); describeUpdateVariable('and regex pattern exists', function(scenario) { - scenario.setup(function() { + scenario.setup(() => { scenario.variableModel = { type: 'query', query: 'apps.*', name: 'test' }; scenario.variableModel.regex = '/apps.*(backend_[0-9]+)/'; scenario.queryResult = [ @@ -295,13 +277,13 @@ describe('VariableSrv', function() { ]; }); - it('should extract and use match group', function() { + it('should extract and use match group', () => { expect(scenario.variable.options[0].value).toBe('backend_01'); }); }); describeUpdateVariable('and regex pattern exists and no match', function(scenario) { - scenario.setup(function() { + scenario.setup(() => { scenario.variableModel = { type: 'query', query: 'apps.*', name: 'test' }; scenario.variableModel.regex = '/apps.*(backendasd[0-9]+)/'; scenario.queryResult = [ @@ -310,14 +292,14 @@ describe('VariableSrv', function() { ]; }); - it('should not add non matching items, None option should be added instead', function() { + it('should not add non matching items, None option should be added instead', () => { expect(scenario.variable.options.length).toBe(1); expect(scenario.variable.options[0].isNone).toBe(true); }); }); describeUpdateVariable('regex pattern without slashes', function(scenario) { - scenario.setup(function() { + scenario.setup(() => { scenario.variableModel = { type: 'query', query: 'apps.*', name: 'test' }; scenario.variableModel.regex = 'backend_01'; scenario.queryResult = [ @@ -326,13 +308,13 @@ describe('VariableSrv', function() { ]; }); - it('should return matches options', function() { + it('should return matches options', () => { expect(scenario.variable.options.length).toBe(1); }); }); describeUpdateVariable('regex pattern remove duplicates', function(scenario) { - scenario.setup(function() { + scenario.setup(() => { scenario.variableModel = { type: 'query', query: 'apps.*', name: 'test' }; scenario.variableModel.regex = '/backend_01/'; scenario.queryResult = [ @@ -341,13 +323,13 @@ describe('VariableSrv', function() { ]; }); - it('should return matches options', function() { + it('should return matches options', () => { expect(scenario.variable.options.length).toBe(1); }); }); describeUpdateVariable('with include All', function(scenario) { - scenario.setup(function() { + scenario.setup(() => { scenario.variableModel = { type: 'query', query: 'apps.*', @@ -357,14 +339,14 @@ describe('VariableSrv', function() { scenario.queryResult = [{ text: 'backend1' }, { text: 'backend2' }, { text: 'backend3' }]; }); - it('should add All option', function() { + it('should add All option', () => { expect(scenario.variable.options[0].text).toBe('All'); expect(scenario.variable.options[0].value).toBe('$__all'); }); }); describeUpdateVariable('with include all and custom value', function(scenario) { - scenario.setup(function() { + scenario.setup(() => { scenario.variableModel = { type: 'query', query: 'apps.*', @@ -375,13 +357,13 @@ describe('VariableSrv', function() { scenario.queryResult = [{ text: 'backend1' }, { text: 'backend2' }, { text: 'backend3' }]; }); - it('should add All option with custom value', function() { + it('should add All option with custom value', () => { expect(scenario.variable.options[0].value).toBe('$__all'); }); }); describeUpdateVariable('without sort', function(scenario) { - scenario.setup(function() { + scenario.setup(() => { scenario.variableModel = { type: 'query', query: 'apps.*', @@ -391,7 +373,7 @@ describe('VariableSrv', function() { scenario.queryResult = [{ text: 'bbb2' }, { text: 'aaa10' }, { text: 'ccc3' }]; }); - it('should return options without sort', function() { + it('should return options without sort', () => { expect(scenario.variable.options[0].text).toBe('bbb2'); expect(scenario.variable.options[1].text).toBe('aaa10'); expect(scenario.variable.options[2].text).toBe('ccc3'); @@ -399,7 +381,7 @@ describe('VariableSrv', function() { }); describeUpdateVariable('with alphabetical sort (asc)', function(scenario) { - scenario.setup(function() { + scenario.setup(() => { scenario.variableModel = { type: 'query', query: 'apps.*', @@ -409,7 +391,7 @@ describe('VariableSrv', function() { scenario.queryResult = [{ text: 'bbb2' }, { text: 'aaa10' }, { text: 'ccc3' }]; }); - it('should return options with alphabetical sort', function() { + it('should return options with alphabetical sort', () => { expect(scenario.variable.options[0].text).toBe('aaa10'); expect(scenario.variable.options[1].text).toBe('bbb2'); expect(scenario.variable.options[2].text).toBe('ccc3'); @@ -417,7 +399,7 @@ describe('VariableSrv', function() { }); describeUpdateVariable('with alphabetical sort (desc)', function(scenario) { - scenario.setup(function() { + scenario.setup(() => { scenario.variableModel = { type: 'query', query: 'apps.*', @@ -427,7 +409,7 @@ describe('VariableSrv', function() { scenario.queryResult = [{ text: 'bbb2' }, { text: 'aaa10' }, { text: 'ccc3' }]; }); - it('should return options with alphabetical sort', function() { + it('should return options with alphabetical sort', () => { expect(scenario.variable.options[0].text).toBe('ccc3'); expect(scenario.variable.options[1].text).toBe('bbb2'); expect(scenario.variable.options[2].text).toBe('aaa10'); @@ -435,7 +417,7 @@ describe('VariableSrv', function() { }); describeUpdateVariable('with numerical sort (asc)', function(scenario) { - scenario.setup(function() { + scenario.setup(() => { scenario.variableModel = { type: 'query', query: 'apps.*', @@ -445,7 +427,7 @@ describe('VariableSrv', function() { scenario.queryResult = [{ text: 'bbb2' }, { text: 'aaa10' }, { text: 'ccc3' }]; }); - it('should return options with numerical sort', function() { + it('should return options with numerical sort', () => { expect(scenario.variable.options[0].text).toBe('bbb2'); expect(scenario.variable.options[1].text).toBe('ccc3'); expect(scenario.variable.options[2].text).toBe('aaa10'); @@ -453,7 +435,7 @@ describe('VariableSrv', function() { }); describeUpdateVariable('with numerical sort (desc)', function(scenario) { - scenario.setup(function() { + scenario.setup(() => { scenario.variableModel = { type: 'query', query: 'apps.*', @@ -463,7 +445,7 @@ describe('VariableSrv', function() { scenario.queryResult = [{ text: 'bbb2' }, { text: 'aaa10' }, { text: 'ccc3' }]; }); - it('should return options with numerical sort', function() { + it('should return options with numerical sort', () => { expect(scenario.variable.options[0].text).toBe('aaa10'); expect(scenario.variable.options[1].text).toBe('ccc3'); expect(scenario.variable.options[2].text).toBe('bbb2'); @@ -474,7 +456,7 @@ describe('VariableSrv', function() { // datasource variable update // describeUpdateVariable('datasource variable with regex filter', function(scenario) { - scenario.setup(function() { + scenario.setup(() => { scenario.variableModel = { type: 'datasource', query: 'graphite', @@ -490,13 +472,13 @@ describe('VariableSrv', function() { ]; }); - it('should set only contain graphite ds and filtered using regex', function() { + it('should set only contain graphite ds and filtered using regex', () => { expect(scenario.variable.options.length).toBe(2); expect(scenario.variable.options[0].value).toBe('backend2_pee'); expect(scenario.variable.options[1].value).toBe('backend4_pee'); }); - it('should keep current value if available', function() { + it('should keep current value if available', () => { expect(scenario.variable.current.value).toBe('backend4_pee'); }); }); @@ -505,7 +487,7 @@ describe('VariableSrv', function() { // Custom variable update // describeUpdateVariable('update custom variable', function(scenario) { - scenario.setup(function() { + scenario.setup(() => { scenario.variableModel = { type: 'custom', query: 'hej, hop, asd', @@ -513,17 +495,17 @@ describe('VariableSrv', function() { }; }); - it('should update options array', function() { + it('should update options array', () => { expect(scenario.variable.options.length).toBe(3); expect(scenario.variable.options[0].text).toBe('hej'); expect(scenario.variable.options[1].value).toBe('hop'); }); }); - describe('multiple interval variables with auto', function() { + describe('multiple interval variables with auto', () => { var variable1, variable2; - beforeEach(function() { + beforeEach(() => { var range = { from: moment(new Date()) .subtract(7, 'days') @@ -558,7 +540,7 @@ describe('VariableSrv', function() { // ctx.$rootScope.$digest(); }); - it('should update options array', function() { + it('should update options array', () => { expect(variable1.options.length).toBe(5); expect(variable1.options[0].text).toBe('auto'); expect(variable1.options[0].value).toBe('$__auto_interval_variable1'); @@ -567,7 +549,7 @@ describe('VariableSrv', function() { expect(variable2.options[0].value).toBe('$__auto_interval_variable2'); }); - it('should correctly set $__auto_interval_variableX', function() { + it('should correctly set $__auto_interval_variableX', () => { var variable1Set, variable2Set, legacySet, From 9f87f6081af9945ea2aa1099c897bc6458c5f42d Mon Sep 17 00:00:00 2001 From: Tobias Skarhed Date: Wed, 1 Aug 2018 15:39:59 +0200 Subject: [PATCH 305/786] Remove Karma test --- .../templating/specs/variable_srv_specs.ts | 568 ------------------ 1 file changed, 568 deletions(-) delete mode 100644 public/app/features/templating/specs/variable_srv_specs.ts diff --git a/public/app/features/templating/specs/variable_srv_specs.ts b/public/app/features/templating/specs/variable_srv_specs.ts deleted file mode 100644 index 6ab5dcad20e..00000000000 --- a/public/app/features/templating/specs/variable_srv_specs.ts +++ /dev/null @@ -1,568 +0,0 @@ -import { describe, beforeEach, it, sinon, expect, angularMocks } from 'test/lib/common'; - -import '../all'; - -import moment from 'moment'; -import helpers from 'test/specs/helpers'; -import { Emitter } from 'app/core/core'; - -describe('VariableSrv', function() { - var ctx = new helpers.ControllerTestContext(); - - beforeEach(angularMocks.module('grafana.core')); - beforeEach(angularMocks.module('grafana.controllers')); - beforeEach(angularMocks.module('grafana.services')); - - beforeEach(ctx.providePhase(['datasourceSrv', 'timeSrv', 'templateSrv', '$location'])); - beforeEach( - angularMocks.inject(($rootScope, $q, $location, $injector) => { - ctx.$q = $q; - ctx.$rootScope = $rootScope; - ctx.$location = $location; - ctx.variableSrv = $injector.get('variableSrv'); - ctx.variableSrv.init({ - templating: { list: [] }, - events: new Emitter(), - updateSubmenuVisibility: sinon.stub(), - }); - ctx.$rootScope.$digest(); - }) - ); - - function describeUpdateVariable(desc, fn) { - describe(desc, function() { - var scenario: any = {}; - scenario.setup = function(setupFn) { - scenario.setupFn = setupFn; - }; - - beforeEach(function() { - scenario.setupFn(); - var ds: any = {}; - ds.metricFindQuery = sinon.stub().returns(ctx.$q.when(scenario.queryResult)); - ctx.datasourceSrv.get = sinon.stub().returns(ctx.$q.when(ds)); - ctx.datasourceSrv.getMetricSources = sinon.stub().returns(scenario.metricSources); - - scenario.variable = ctx.variableSrv.createVariableFromModel(scenario.variableModel); - ctx.variableSrv.addVariable(scenario.variable); - - ctx.variableSrv.updateOptions(scenario.variable); - ctx.$rootScope.$digest(); - }); - - fn(scenario); - }); - } - - describeUpdateVariable('interval variable without auto', scenario => { - scenario.setup(() => { - scenario.variableModel = { - type: 'interval', - query: '1s,2h,5h,1d', - name: 'test', - }; - }); - - it('should update options array', () => { - expect(scenario.variable.options.length).to.be(4); - expect(scenario.variable.options[0].text).to.be('1s'); - expect(scenario.variable.options[0].value).to.be('1s'); - }); - }); - - // - // Interval variable update - // - describeUpdateVariable('interval variable with auto', scenario => { - scenario.setup(() => { - scenario.variableModel = { - type: 'interval', - query: '1s,2h,5h,1d', - name: 'test', - auto: true, - auto_count: 10, - }; - - var range = { - from: moment(new Date()) - .subtract(7, 'days') - .toDate(), - to: new Date(), - }; - - ctx.timeSrv.timeRange = sinon.stub().returns(range); - ctx.templateSrv.setGrafanaVariable = sinon.spy(); - }); - - it('should update options array', function() { - expect(scenario.variable.options.length).to.be(5); - expect(scenario.variable.options[0].text).to.be('auto'); - expect(scenario.variable.options[0].value).to.be('$__auto_interval_test'); - }); - - it('should set $__auto_interval_test', function() { - var call = ctx.templateSrv.setGrafanaVariable.firstCall; - expect(call.args[0]).to.be('$__auto_interval_test'); - expect(call.args[1]).to.be('12h'); - }); - - // updateAutoValue() gets called twice: once directly once via VariableSrv.validateVariableSelectionState() - // So use lastCall instead of a specific call number - it('should set $__auto_interval', function() { - var call = ctx.templateSrv.setGrafanaVariable.lastCall; - expect(call.args[0]).to.be('$__auto_interval'); - expect(call.args[1]).to.be('12h'); - }); - }); - - // - // Query variable update - // - describeUpdateVariable('query variable with empty current object and refresh', function(scenario) { - scenario.setup(function() { - scenario.variableModel = { - type: 'query', - query: '', - name: 'test', - current: {}, - }; - scenario.queryResult = [{ text: 'backend1' }, { text: 'backend2' }]; - }); - - it('should set current value to first option', function() { - expect(scenario.variable.options.length).to.be(2); - expect(scenario.variable.current.value).to.be('backend1'); - }); - }); - - describeUpdateVariable( - 'query variable with multi select and new options does not contain some selected values', - function(scenario) { - scenario.setup(function() { - scenario.variableModel = { - type: 'query', - query: '', - name: 'test', - current: { - value: ['val1', 'val2', 'val3'], - text: 'val1 + val2 + val3', - }, - }; - scenario.queryResult = [{ text: 'val2' }, { text: 'val3' }]; - }); - - it('should update current value', function() { - expect(scenario.variable.current.value).to.eql(['val2', 'val3']); - expect(scenario.variable.current.text).to.eql('val2 + val3'); - }); - } - ); - - describeUpdateVariable( - 'query variable with multi select and new options does not contain any selected values', - function(scenario) { - scenario.setup(function() { - scenario.variableModel = { - type: 'query', - query: '', - name: 'test', - current: { - value: ['val1', 'val2', 'val3'], - text: 'val1 + val2 + val3', - }, - }; - scenario.queryResult = [{ text: 'val5' }, { text: 'val6' }]; - }); - - it('should update current value with first one', function() { - expect(scenario.variable.current.value).to.eql('val5'); - expect(scenario.variable.current.text).to.eql('val5'); - }); - } - ); - - describeUpdateVariable('query variable with multi select and $__all selected', function(scenario) { - scenario.setup(function() { - scenario.variableModel = { - type: 'query', - query: '', - name: 'test', - includeAll: true, - current: { - value: ['$__all'], - text: 'All', - }, - }; - scenario.queryResult = [{ text: 'val5' }, { text: 'val6' }]; - }); - - it('should keep current All value', function() { - expect(scenario.variable.current.value).to.eql(['$__all']); - expect(scenario.variable.current.text).to.eql('All'); - }); - }); - - describeUpdateVariable('query variable with numeric results', function(scenario) { - scenario.setup(function() { - scenario.variableModel = { - type: 'query', - query: '', - name: 'test', - current: {}, - }; - scenario.queryResult = [{ text: 12, value: 12 }]; - }); - - it('should set current value to first option', function() { - expect(scenario.variable.current.value).to.be('12'); - expect(scenario.variable.options[0].value).to.be('12'); - expect(scenario.variable.options[0].text).to.be('12'); - }); - }); - - describeUpdateVariable('basic query variable', function(scenario) { - scenario.setup(function() { - scenario.variableModel = { type: 'query', query: 'apps.*', name: 'test' }; - scenario.queryResult = [{ text: 'backend1' }, { text: 'backend2' }]; - }); - - it('should update options array', function() { - expect(scenario.variable.options.length).to.be(2); - expect(scenario.variable.options[0].text).to.be('backend1'); - expect(scenario.variable.options[0].value).to.be('backend1'); - expect(scenario.variable.options[1].value).to.be('backend2'); - }); - - it('should select first option as value', function() { - expect(scenario.variable.current.value).to.be('backend1'); - }); - }); - - describeUpdateVariable('and existing value still exists in options', function(scenario) { - scenario.setup(function() { - scenario.variableModel = { type: 'query', query: 'apps.*', name: 'test' }; - scenario.variableModel.current = { value: 'backend2', text: 'backend2' }; - scenario.queryResult = [{ text: 'backend1' }, { text: 'backend2' }]; - }); - - it('should keep variable value', function() { - expect(scenario.variable.current.text).to.be('backend2'); - }); - }); - - describeUpdateVariable('and regex pattern exists', function(scenario) { - scenario.setup(function() { - scenario.variableModel = { type: 'query', query: 'apps.*', name: 'test' }; - scenario.variableModel.regex = '/apps.*(backend_[0-9]+)/'; - scenario.queryResult = [ - { text: 'apps.backend.backend_01.counters.req' }, - { text: 'apps.backend.backend_02.counters.req' }, - ]; - }); - - it('should extract and use match group', function() { - expect(scenario.variable.options[0].value).to.be('backend_01'); - }); - }); - - describeUpdateVariable('and regex pattern exists and no match', function(scenario) { - scenario.setup(function() { - scenario.variableModel = { type: 'query', query: 'apps.*', name: 'test' }; - scenario.variableModel.regex = '/apps.*(backendasd[0-9]+)/'; - scenario.queryResult = [ - { text: 'apps.backend.backend_01.counters.req' }, - { text: 'apps.backend.backend_02.counters.req' }, - ]; - }); - - it('should not add non matching items, None option should be added instead', function() { - expect(scenario.variable.options.length).to.be(1); - expect(scenario.variable.options[0].isNone).to.be(true); - }); - }); - - describeUpdateVariable('regex pattern without slashes', function(scenario) { - scenario.setup(function() { - scenario.variableModel = { type: 'query', query: 'apps.*', name: 'test' }; - scenario.variableModel.regex = 'backend_01'; - scenario.queryResult = [ - { text: 'apps.backend.backend_01.counters.req' }, - { text: 'apps.backend.backend_02.counters.req' }, - ]; - }); - - it('should return matches options', function() { - expect(scenario.variable.options.length).to.be(1); - }); - }); - - describeUpdateVariable('regex pattern remove duplicates', function(scenario) { - scenario.setup(function() { - scenario.variableModel = { type: 'query', query: 'apps.*', name: 'test' }; - scenario.variableModel.regex = '/backend_01/'; - scenario.queryResult = [ - { text: 'apps.backend.backend_01.counters.req' }, - { text: 'apps.backend.backend_01.counters.req' }, - ]; - }); - - it('should return matches options', function() { - expect(scenario.variable.options.length).to.be(1); - }); - }); - - describeUpdateVariable('with include All', function(scenario) { - scenario.setup(function() { - scenario.variableModel = { - type: 'query', - query: 'apps.*', - name: 'test', - includeAll: true, - }; - scenario.queryResult = [{ text: 'backend1' }, { text: 'backend2' }, { text: 'backend3' }]; - }); - - it('should add All option', function() { - expect(scenario.variable.options[0].text).to.be('All'); - expect(scenario.variable.options[0].value).to.be('$__all'); - }); - }); - - describeUpdateVariable('with include all and custom value', function(scenario) { - scenario.setup(function() { - scenario.variableModel = { - type: 'query', - query: 'apps.*', - name: 'test', - includeAll: true, - allValue: '*', - }; - scenario.queryResult = [{ text: 'backend1' }, { text: 'backend2' }, { text: 'backend3' }]; - }); - - it('should add All option with custom value', function() { - expect(scenario.variable.options[0].value).to.be('$__all'); - }); - }); - - describeUpdateVariable('without sort', function(scenario) { - scenario.setup(function() { - scenario.variableModel = { - type: 'query', - query: 'apps.*', - name: 'test', - sort: 0, - }; - scenario.queryResult = [{ text: 'bbb2' }, { text: 'aaa10' }, { text: 'ccc3' }]; - }); - - it('should return options without sort', function() { - expect(scenario.variable.options[0].text).to.be('bbb2'); - expect(scenario.variable.options[1].text).to.be('aaa10'); - expect(scenario.variable.options[2].text).to.be('ccc3'); - }); - }); - - describeUpdateVariable('with alphabetical sort (asc)', function(scenario) { - scenario.setup(function() { - scenario.variableModel = { - type: 'query', - query: 'apps.*', - name: 'test', - sort: 1, - }; - scenario.queryResult = [{ text: 'bbb2' }, { text: 'aaa10' }, { text: 'ccc3' }]; - }); - - it('should return options with alphabetical sort', function() { - expect(scenario.variable.options[0].text).to.be('aaa10'); - expect(scenario.variable.options[1].text).to.be('bbb2'); - expect(scenario.variable.options[2].text).to.be('ccc3'); - }); - }); - - describeUpdateVariable('with alphabetical sort (desc)', function(scenario) { - scenario.setup(function() { - scenario.variableModel = { - type: 'query', - query: 'apps.*', - name: 'test', - sort: 2, - }; - scenario.queryResult = [{ text: 'bbb2' }, { text: 'aaa10' }, { text: 'ccc3' }]; - }); - - it('should return options with alphabetical sort', function() { - expect(scenario.variable.options[0].text).to.be('ccc3'); - expect(scenario.variable.options[1].text).to.be('bbb2'); - expect(scenario.variable.options[2].text).to.be('aaa10'); - }); - }); - - describeUpdateVariable('with numerical sort (asc)', function(scenario) { - scenario.setup(function() { - scenario.variableModel = { - type: 'query', - query: 'apps.*', - name: 'test', - sort: 3, - }; - scenario.queryResult = [{ text: 'bbb2' }, { text: 'aaa10' }, { text: 'ccc3' }]; - }); - - it('should return options with numerical sort', function() { - expect(scenario.variable.options[0].text).to.be('bbb2'); - expect(scenario.variable.options[1].text).to.be('ccc3'); - expect(scenario.variable.options[2].text).to.be('aaa10'); - }); - }); - - describeUpdateVariable('with numerical sort (desc)', function(scenario) { - scenario.setup(function() { - scenario.variableModel = { - type: 'query', - query: 'apps.*', - name: 'test', - sort: 4, - }; - scenario.queryResult = [{ text: 'bbb2' }, { text: 'aaa10' }, { text: 'ccc3' }]; - }); - - it('should return options with numerical sort', function() { - expect(scenario.variable.options[0].text).to.be('aaa10'); - expect(scenario.variable.options[1].text).to.be('ccc3'); - expect(scenario.variable.options[2].text).to.be('bbb2'); - }); - }); - - // - // datasource variable update - // - describeUpdateVariable('datasource variable with regex filter', function(scenario) { - scenario.setup(function() { - scenario.variableModel = { - type: 'datasource', - query: 'graphite', - name: 'test', - current: { value: 'backend4_pee', text: 'backend4_pee' }, - regex: '/pee$/', - }; - scenario.metricSources = [ - { name: 'backend1', meta: { id: 'influx' } }, - { name: 'backend2_pee', meta: { id: 'graphite' } }, - { name: 'backend3', meta: { id: 'graphite' } }, - { name: 'backend4_pee', meta: { id: 'graphite' } }, - ]; - }); - - it('should set only contain graphite ds and filtered using regex', function() { - expect(scenario.variable.options.length).to.be(2); - expect(scenario.variable.options[0].value).to.be('backend2_pee'); - expect(scenario.variable.options[1].value).to.be('backend4_pee'); - }); - - it('should keep current value if available', function() { - expect(scenario.variable.current.value).to.be('backend4_pee'); - }); - }); - - // - // Custom variable update - // - describeUpdateVariable('update custom variable', function(scenario) { - scenario.setup(function() { - scenario.variableModel = { - type: 'custom', - query: 'hej, hop, asd', - name: 'test', - }; - }); - - it('should update options array', function() { - expect(scenario.variable.options.length).to.be(3); - expect(scenario.variable.options[0].text).to.be('hej'); - expect(scenario.variable.options[1].value).to.be('hop'); - }); - }); - - describe('multiple interval variables with auto', function() { - var variable1, variable2; - - beforeEach(function() { - var range = { - from: moment(new Date()) - .subtract(7, 'days') - .toDate(), - to: new Date(), - }; - ctx.timeSrv.timeRange = sinon.stub().returns(range); - ctx.templateSrv.setGrafanaVariable = sinon.spy(); - - var variableModel1 = { - type: 'interval', - query: '1s,2h,5h,1d', - name: 'variable1', - auto: true, - auto_count: 10, - }; - variable1 = ctx.variableSrv.createVariableFromModel(variableModel1); - ctx.variableSrv.addVariable(variable1); - - var variableModel2 = { - type: 'interval', - query: '1s,2h,5h', - name: 'variable2', - auto: true, - auto_count: 1000, - }; - variable2 = ctx.variableSrv.createVariableFromModel(variableModel2); - ctx.variableSrv.addVariable(variable2); - - ctx.variableSrv.updateOptions(variable1); - ctx.variableSrv.updateOptions(variable2); - ctx.$rootScope.$digest(); - }); - - it('should update options array', function() { - expect(variable1.options.length).to.be(5); - expect(variable1.options[0].text).to.be('auto'); - expect(variable1.options[0].value).to.be('$__auto_interval_variable1'); - expect(variable2.options.length).to.be(4); - expect(variable2.options[0].text).to.be('auto'); - expect(variable2.options[0].value).to.be('$__auto_interval_variable2'); - }); - - it('should correctly set $__auto_interval_variableX', function() { - var variable1Set, - variable2Set, - legacySet, - unknownSet = false; - // updateAutoValue() gets called repeatedly: once directly once via VariableSrv.validateVariableSelectionState() - // So check that all calls are valid rather than expect a specific number and/or ordering of calls - for (var i = 0; i < ctx.templateSrv.setGrafanaVariable.callCount; i++) { - var call = ctx.templateSrv.setGrafanaVariable.getCall(i); - switch (call.args[0]) { - case '$__auto_interval_variable1': - expect(call.args[1]).to.be('12h'); - variable1Set = true; - break; - case '$__auto_interval_variable2': - expect(call.args[1]).to.be('10m'); - variable2Set = true; - break; - case '$__auto_interval': - expect(call.args[1]).to.match(/^(12h|10m)$/); - legacySet = true; - break; - default: - unknownSet = true; - break; - } - } - expect(variable1Set).to.be.equal(true); - expect(variable2Set).to.be.equal(true); - expect(legacySet).to.be.equal(true); - expect(unknownSet).to.be.equal(false); - }); - }); -}); From 3096905d393b860f63c117c7a2e1dcd5a000f088 Mon Sep 17 00:00:00 2001 From: Leonard Gram Date: Mon, 6 Aug 2018 14:04:41 +0200 Subject: [PATCH 306/786] docs: how to build a docker image. --- README.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/README.md b/README.md index 322523d703b..b2baf0ece59 100644 --- a/README.md +++ b/README.md @@ -74,6 +74,15 @@ bra run Open grafana in your browser (default: `http://localhost:3000`) and login with admin user (default: `user/pass = admin/admin`). +### Building a docker image (on linux/amd64) + +This builds a docker image from your local sources: + +1. Build the frontend `go run build.go build-frontend` +2. Build the docker image `make build-docker-dev` + +The resulting image will be tagged as `grafana/grafana:dev` + ### Dev config Create a custom.ini in the conf directory to override default configuration options. From 5da3584dd4fee9a681ee0c599bd551849770cd46 Mon Sep 17 00:00:00 2001 From: David Date: Mon, 6 Aug 2018 14:36:02 +0200 Subject: [PATCH 307/786] Explore: facetting for label completion (#12786) * Explore: facetting for label completion - unified metric and non-metric label completion - label keys and values are now fetched fresh for each valid selector - complete selector means only values are suggested that are supported by the selector - properly implemented metric lookup for selectors (until the first metric was used which breaks when multiple metrics are present) - typeahead tests now need a valid selection to demark the cursor * Fix facetting queries for empty selector --- .../Explore/PromQueryField.jest.tsx | 92 +++++++++--- .../app/containers/Explore/PromQueryField.tsx | 136 +++++++++--------- public/app/containers/Explore/QueryField.tsx | 3 + .../Explore/utils/prometheus.jest.ts | 33 +++++ .../containers/Explore/utils/prometheus.ts | 70 ++++++++- 5 files changed, 248 insertions(+), 86 deletions(-) create mode 100644 public/app/containers/Explore/utils/prometheus.jest.ts diff --git a/public/app/containers/Explore/PromQueryField.jest.tsx b/public/app/containers/Explore/PromQueryField.jest.tsx index 8d2903cb2c2..cd0d940961e 100644 --- a/public/app/containers/Explore/PromQueryField.jest.tsx +++ b/public/app/containers/Explore/PromQueryField.jest.tsx @@ -1,11 +1,12 @@ import React from 'react'; import Enzyme, { shallow } from 'enzyme'; import Adapter from 'enzyme-adapter-react-16'; - -Enzyme.configure({ adapter: new Adapter() }); +import Plain from 'slate-plain-serializer'; import PromQueryField from './PromQueryField'; +Enzyme.configure({ adapter: new Adapter() }); + describe('PromQueryField typeahead handling', () => { const defaultProps = { request: () => ({ data: { data: [] } }), @@ -59,20 +60,35 @@ describe('PromQueryField typeahead handling', () => { describe('label suggestions', () => { it('returns default label suggestions on label context and no metric', () => { const instance = shallow().instance() as PromQueryField; - const result = instance.getTypeahead({ text: 'j', prefix: 'j', wrapperClasses: ['context-labels'] }); + const value = Plain.deserialize('{}'); + const range = value.selection.merge({ + anchorOffset: 1, + }); + const valueWithSelection = value.change().select(range).value; + const result = instance.getTypeahead({ + text: '', + prefix: '', + wrapperClasses: ['context-labels'], + value: valueWithSelection, + }); expect(result.context).toBe('context-labels'); expect(result.suggestions).toEqual([{ items: [{ label: 'job' }, { label: 'instance' }], label: 'Labels' }]); }); it('returns label suggestions on label context and metric', () => { const instance = shallow( - + ).instance() as PromQueryField; + const value = Plain.deserialize('metric{}'); + const range = value.selection.merge({ + anchorOffset: 7, + }); + const valueWithSelection = value.change().select(range).value; const result = instance.getTypeahead({ - text: 'job', - prefix: 'job', + text: '', + prefix: '', wrapperClasses: ['context-labels'], - metric: 'foo', + value: valueWithSelection, }); expect(result.context).toBe('context-labels'); expect(result.suggestions).toEqual([{ items: [{ label: 'bar' }], label: 'Labels' }]); @@ -80,13 +96,18 @@ describe('PromQueryField typeahead handling', () => { it('returns a refresher on label context and unavailable metric', () => { const instance = shallow( - + ).instance() as PromQueryField; + const value = Plain.deserialize('metric{}'); + const range = value.selection.merge({ + anchorOffset: 7, + }); + const valueWithSelection = value.change().select(range).value; const result = instance.getTypeahead({ - text: 'job', - prefix: 'job', + text: '', + prefix: '', wrapperClasses: ['context-labels'], - metric: 'xxx', + value: valueWithSelection, }); expect(result.context).toBeUndefined(); expect(result.refresher).toBeInstanceOf(Promise); @@ -95,28 +116,61 @@ describe('PromQueryField typeahead handling', () => { it('returns label values on label context when given a metric and a label key', () => { const instance = shallow( - + ).instance() as PromQueryField; + const value = Plain.deserialize('metric{bar=ba}'); + const range = value.selection.merge({ + anchorOffset: 13, + }); + const valueWithSelection = value.change().select(range).value; const result = instance.getTypeahead({ text: '=ba', prefix: 'ba', wrapperClasses: ['context-labels'], - metric: 'foo', labelKey: 'bar', + value: valueWithSelection, }); expect(result.context).toBe('context-label-values'); - expect(result.suggestions).toEqual([{ items: [{ label: 'baz' }], label: 'Label values' }]); + expect(result.suggestions).toEqual([{ items: [{ label: 'baz' }], label: 'Label values for "bar"' }]); }); - it('returns label suggestions on aggregation context and metric', () => { + it('returns label suggestions on aggregation context and metric w/ selector', () => { const instance = shallow( - + ).instance() as PromQueryField; + const value = Plain.deserialize('sum(metric{foo="xx"}) by ()'); + const range = value.selection.merge({ + anchorOffset: 26, + }); + const valueWithSelection = value.change().select(range).value; const result = instance.getTypeahead({ - text: 'job', - prefix: 'job', + text: '', + prefix: '', wrapperClasses: ['context-aggregation'], - metric: 'foo', + value: valueWithSelection, + }); + expect(result.context).toBe('context-aggregation'); + expect(result.suggestions).toEqual([{ items: [{ label: 'bar' }], label: 'Labels' }]); + }); + + it('returns label suggestions on aggregation context and metric w/o selector', () => { + const instance = shallow( + + ).instance() as PromQueryField; + const value = Plain.deserialize('sum(metric) by ()'); + const range = value.selection.merge({ + anchorOffset: 16, + }); + const valueWithSelection = value.change().select(range).value; + const result = instance.getTypeahead({ + text: '', + prefix: '', + wrapperClasses: ['context-aggregation'], + value: valueWithSelection, }); expect(result.context).toBe('context-aggregation'); expect(result.suggestions).toEqual([{ items: [{ label: 'bar' }], label: 'Labels' }]); diff --git a/public/app/containers/Explore/PromQueryField.tsx b/public/app/containers/Explore/PromQueryField.tsx index eb8fc25c67f..c6119cc9d0f 100644 --- a/public/app/containers/Explore/PromQueryField.tsx +++ b/public/app/containers/Explore/PromQueryField.tsx @@ -1,12 +1,13 @@ import _ from 'lodash'; import React from 'react'; +import { Value } from 'slate'; // dom also includes Element polyfills import { getNextCharacter, getPreviousCousin } from './utils/dom'; import PluginPrism, { setPrismTokens } from './slate-plugins/prism/index'; import PrismPromql, { FUNCTIONS } from './slate-plugins/prism/promql'; import RunnerPlugin from './slate-plugins/runner'; -import { processLabels, RATE_RANGES, cleanText } from './utils/prometheus'; +import { processLabels, RATE_RANGES, cleanText, getCleanSelector } from './utils/prometheus'; import TypeaheadField, { Suggestion, @@ -16,7 +17,8 @@ import TypeaheadField, { TypeaheadOutput, } from './QueryField'; -const EMPTY_METRIC = ''; +const DEFAULT_KEYS = ['job', 'instance']; +const EMPTY_SELECTOR = '{}'; const METRIC_MARK = 'metric'; const PRISM_LANGUAGE = 'promql'; @@ -77,8 +79,8 @@ interface PromTypeaheadInput { text: string; prefix: string; wrapperClasses: string[]; - metric?: string; labelKey?: string; + value?: Value; } class PromQueryField extends React.Component { @@ -119,25 +121,23 @@ class PromQueryField extends React.Component { - const { editorNode, prefix, text, wrapperNode } = typeahead; + const { prefix, text, value, wrapperNode } = typeahead; // Get DOM-dependent context const wrapperClasses = Array.from(wrapperNode.classList); - // Take first metric as lucky guess - const metricNode = editorNode.querySelector(`.${METRIC_MARK}`); - const metric = metricNode && metricNode.textContent; const labelKeyNode = getPreviousCousin(wrapperNode, '.attr-name'); const labelKey = labelKeyNode && labelKeyNode.textContent; + const nextChar = getNextCharacter(); - const result = this.getTypeahead({ text, prefix, wrapperClasses, metric, labelKey }); + const result = this.getTypeahead({ text, value, prefix, wrapperClasses, labelKey }); - console.log('handleTypeahead', wrapperClasses, text, prefix, result.context); + console.log('handleTypeahead', wrapperClasses, text, prefix, nextChar, labelKey, result.context); return result; }; // Keep this DOM-free for testing - getTypeahead({ prefix, wrapperClasses, metric, text }: PromTypeaheadInput): TypeaheadOutput { + getTypeahead({ prefix, wrapperClasses, text }: PromTypeaheadInput): TypeaheadOutput { // Determine candidates by CSS context if (_.includes(wrapperClasses, 'context-range')) { // Suggestions for metric[|] @@ -145,12 +145,11 @@ class PromQueryField extends React.Component = null; const suggestions: SuggestionGroup[] = []; - const labelKeys = this.state.labelKeys[metric]; + + // sum(foo{bar="1"}) by (|) + const line = value.anchorBlock.getText(); + const cursorOffset: number = value.anchorOffset; + // sum(foo{bar="1"}) by ( + const leftSide = line.slice(0, cursorOffset); + const openParensAggregationIndex = leftSide.lastIndexOf('('); + const openParensSelectorIndex = leftSide.slice(0, openParensAggregationIndex).lastIndexOf('('); + const closeParensSelectorIndex = leftSide.slice(openParensSelectorIndex).indexOf(')') + openParensSelectorIndex; + // foo{bar="1"} + const selectorString = leftSide.slice(openParensSelectorIndex + 1, closeParensSelectorIndex); + const selector = getCleanSelector(selectorString, selectorString.length - 2); + + const labelKeys = this.state.labelKeys[selector]; if (labelKeys) { suggestions.push({ label: 'Labels', items: labelKeys.map(wrapLabel) }); } else { - refresher = this.fetchMetricLabels(metric); + refresher = this.fetchSeriesLabels(selector); } return { @@ -208,59 +220,51 @@ class PromQueryField extends React.Component = null; const suggestions: SuggestionGroup[] = []; - if (metric) { - const labelKeys = this.state.labelKeys[metric]; - if (labelKeys) { - if ((text && text.startsWith('=')) || _.includes(wrapperClasses, 'attr-value')) { - // Label values - if (labelKey) { - const labelValues = this.state.labelValues[metric][labelKey]; - context = 'context-label-values'; - suggestions.push({ - label: 'Label values', - items: labelValues.map(wrapLabel), - }); - } - } else { - // Label keys - context = 'context-labels'; - suggestions.push({ label: 'Labels', items: labelKeys.map(wrapLabel) }); - } - } else { - refresher = this.fetchMetricLabels(metric); + const line = value.anchorBlock.getText(); + const cursorOffset: number = value.anchorOffset; + + // Get normalized selector + let selector; + try { + selector = getCleanSelector(line, cursorOffset); + } catch { + selector = EMPTY_SELECTOR; + } + const containsMetric = selector.indexOf('__name__=') > -1; + + if ((text && text.startsWith('=')) || _.includes(wrapperClasses, 'attr-value')) { + // Label values + if (labelKey && this.state.labelValues[selector] && this.state.labelValues[selector][labelKey]) { + const labelValues = this.state.labelValues[selector][labelKey]; + context = 'context-label-values'; + suggestions.push({ + label: `Label values for "${labelKey}"`, + items: labelValues.map(wrapLabel), + }); } } else { - // Metric-independent label queries - const defaultKeys = ['job', 'instance']; - // Munge all keys that we have seen together - const labelKeys = Object.keys(this.state.labelKeys).reduce((acc, metric) => { - return acc.concat(this.state.labelKeys[metric].filter(key => acc.indexOf(key) === -1)); - }, defaultKeys); - if ((text && text.startsWith('=')) || _.includes(wrapperClasses, 'attr-value')) { - // Label values - if (labelKey) { - if (this.state.labelValues[EMPTY_METRIC]) { - const labelValues = this.state.labelValues[EMPTY_METRIC][labelKey]; - context = 'context-label-values'; - suggestions.push({ - label: 'Label values', - items: labelValues.map(wrapLabel), - }); - } else { - // Can only query label values for now (API to query keys is under development) - refresher = this.fetchLabelValues(labelKey); - } - } - } else { - // Label keys + // Label keys + const labelKeys = this.state.labelKeys[selector] || (containsMetric ? null : DEFAULT_KEYS); + if (labelKeys) { context = 'context-labels'; - suggestions.push({ label: 'Labels', items: labelKeys.map(wrapLabel) }); + suggestions.push({ label: `Labels`, items: labelKeys.map(wrapLabel) }); } } + + // Query labels for selector + if (selector && !this.state.labelValues[selector]) { + if (selector === EMPTY_SELECTOR) { + // Query label values for default labels + refresher = Promise.all(DEFAULT_KEYS.map(key => this.fetchLabelValues(key))); + } else { + refresher = this.fetchSeriesLabels(selector, !containsMetric); + } + } + return { context, refresher, suggestions }; } @@ -276,14 +280,14 @@ class PromQueryField extends React.Component { const selection = window.getSelection(); const { cleanText, onTypeahead } = this.props; + const { value } = this.state; if (onTypeahead && selection.anchorNode) { const wrapperNode = selection.anchorNode.parentElement; @@ -221,6 +223,7 @@ class QueryField extends React.Component { + it('returns a clean selector from an empty selector', () => { + expect(getCleanSelector('{}', 1)).toBe('{}'); + }); + it('throws if selector is broken', () => { + expect(() => getCleanSelector('{foo')).toThrow(); + }); + it('returns the selector sorted by label key', () => { + expect(getCleanSelector('{foo="bar"}')).toBe('{foo="bar"}'); + expect(getCleanSelector('{foo="bar",baz="xx"}')).toBe('{baz="xx",foo="bar"}'); + }); + it('returns a clean selector from an incomplete one', () => { + expect(getCleanSelector('{foo}')).toBe('{}'); + expect(getCleanSelector('{foo="bar",baz}')).toBe('{foo="bar"}'); + expect(getCleanSelector('{foo="bar",baz="}')).toBe('{foo="bar"}'); + }); + it('throws if not inside a selector', () => { + expect(() => getCleanSelector('foo{}', 0)).toThrow(); + expect(() => getCleanSelector('foo{} + bar{}', 5)).toThrow(); + }); + it('returns the selector nearest to the cursor offset', () => { + expect(() => getCleanSelector('{foo="bar"} + {foo="bar"}', 0)).toThrow(); + expect(getCleanSelector('{foo="bar"} + {foo="bar"}', 1)).toBe('{foo="bar"}'); + expect(getCleanSelector('{foo="bar"} + {baz="xx"}', 1)).toBe('{foo="bar"}'); + expect(getCleanSelector('{baz="xx"} + {foo="bar"}', 16)).toBe('{foo="bar"}'); + }); + it('returns a selector with metric if metric is given', () => { + expect(getCleanSelector('bar{foo}', 4)).toBe('{__name__="bar"}'); + expect(getCleanSelector('baz{foo="bar"}', 12)).toBe('{__name__="baz",foo="bar"}'); + }); +}); diff --git a/public/app/containers/Explore/utils/prometheus.ts b/public/app/containers/Explore/utils/prometheus.ts index 30f9c25b8f7..ab77271076d 100644 --- a/public/app/containers/Explore/utils/prometheus.ts +++ b/public/app/containers/Explore/utils/prometheus.ts @@ -1,9 +1,16 @@ export const RATE_RANGES = ['1m', '5m', '10m', '30m', '1h']; -export function processLabels(labels) { +export function processLabels(labels, withName = false) { const values = {}; labels.forEach(l => { const { __name__, ...rest } = l; + if (withName) { + values['__name__'] = values['__name__'] || []; + if (values['__name__'].indexOf(__name__) === -1) { + values['__name__'].push(__name__); + } + } + Object.keys(rest).forEach(key => { if (!values[key]) { values[key] = []; @@ -18,3 +25,64 @@ export function processLabels(labels) { // Strip syntax chars export const cleanText = s => s.replace(/[{}[\]="(),!~+\-*/^%]/g, '').trim(); + +// const cleanSelectorRegexp = /\{(\w+="[^"\n]*?")(,\w+="[^"\n]*?")*\}/; +const selectorRegexp = /\{[^}]*?\}/; +const labelRegexp = /\b\w+="[^"\n]*?"/g; +export function getCleanSelector(query: string, cursorOffset = 1): string { + if (!query.match(selectorRegexp)) { + // Special matcher for metrics + if (query.match(/^\w+$/)) { + return `{__name__="${query}"}`; + } + throw new Error('Query must contain a selector: ' + query); + } + + // Check if inside a selector + const prefix = query.slice(0, cursorOffset); + const prefixOpen = prefix.lastIndexOf('{'); + const prefixClose = prefix.lastIndexOf('}'); + if (prefixOpen === -1) { + throw new Error('Not inside selector, missing open brace: ' + prefix); + } + if (prefixClose > -1 && prefixClose > prefixOpen) { + throw new Error('Not inside selector, previous selector already closed: ' + prefix); + } + const suffix = query.slice(cursorOffset); + const suffixCloseIndex = suffix.indexOf('}'); + const suffixClose = suffixCloseIndex + cursorOffset; + const suffixOpenIndex = suffix.indexOf('{'); + const suffixOpen = suffixOpenIndex + cursorOffset; + if (suffixClose === -1) { + throw new Error('Not inside selector, missing closing brace in suffix: ' + suffix); + } + if (suffixOpenIndex > -1 && suffixOpen < suffixClose) { + throw new Error('Not inside selector, next selector opens before this one closed: ' + suffix); + } + + // Extract clean labels to form clean selector, incomplete labels are dropped + const selector = query.slice(prefixOpen, suffixClose); + let labels = {}; + selector.replace(labelRegexp, match => { + const delimiterIndex = match.indexOf('='); + const key = match.slice(0, delimiterIndex); + const value = match.slice(delimiterIndex + 1, match.length); + labels[key] = value; + return ''; + }); + + // Add metric if there is one before the selector + const metricPrefix = query.slice(0, prefixOpen); + const metricMatch = metricPrefix.match(/\w+$/); + if (metricMatch) { + labels['__name__'] = `"${metricMatch[0]}"`; + } + + // Build sorted selector + const cleanSelector = Object.keys(labels) + .sort() + .map(key => `${key}=${labels[key]}`) + .join(','); + + return ['{', cleanSelector, '}'].join(''); +} From 4e33314c141c0267d0a44e44497bb2c6edca3e99 Mon Sep 17 00:00:00 2001 From: dadosch Date: Mon, 6 Aug 2018 14:40:30 +0200 Subject: [PATCH 308/786] unix socket docs --- docs/sources/installation/configuration.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/sources/installation/configuration.md b/docs/sources/installation/configuration.md index 2a799b044b3..d81d8a8dcec 100644 --- a/docs/sources/installation/configuration.md +++ b/docs/sources/installation/configuration.md @@ -181,7 +181,7 @@ embedded database (included in the main Grafana binary). ### url -Use either URL or or the other fields below to configure the database +Use either URL or the other fields below to configure the database Example: `mysql://user:secret@host:port/database` ### type @@ -195,9 +195,9 @@ will be stored. ### host -Only applicable to MySQL or Postgres. Includes IP or hostname and port. +Only applicable to MySQL or Postgres. Includes IP or hostname and port or in case of unix sockets the path to it. For example, for MySQL running on the same host as Grafana: `host = -127.0.0.1:3306` +127.0.0.1:3306` or with unix sockets: `host = /var/run/mysqld/mysqld.sock` ### name @@ -697,9 +697,9 @@ session provider you have configured. - **file:** session file path, e.g. `data/sessions` - **mysql:** go-sql-driver/mysql dsn config string, e.g. `user:password@tcp(127.0.0.1:3306)/database_name` -- **postgres:** ex: user=a password=b host=localhost port=5432 dbname=c sslmode=verify-full -- **memcache:** ex: 127.0.0.1:11211 -- **redis:** ex: `addr=127.0.0.1:6379,pool_size=100,prefix=grafana` +- **postgres:** ex: `user=a password=b host=localhost port=5432 dbname=c sslmode=verify-full` +- **memcache:** ex: `127.0.0.1:11211` +- **redis:** ex: `addr=127.0.0.1:6379,pool_size=100,prefix=grafana`. For unix socket, use for example: `network=unix,addr=/var/run/redis/redis.sock,pool_size=100,db=grafana` Postgres valid `sslmode` are `disable`, `require`, `verify-ca`, and `verify-full` (default). From eaff7b0f68844769266f6fd795644b1d058c837a Mon Sep 17 00:00:00 2001 From: David Kaltschmidt Date: Thu, 2 Aug 2018 16:43:33 +0200 Subject: [PATCH 309/786] Explore: Add history to query fields - queries are saved to localstorage history array - one history per datasource type (plugin ID) - 100 items kept with timestamps - history suggestions can be pulled up with Ctrl-SPACE --- public/app/containers/Explore/Explore.tsx | 50 ++++++++++++++++--- .../app/containers/Explore/PromQueryField.tsx | 45 ++++++++++++++++- public/app/containers/Explore/QueryField.tsx | 8 ++- public/app/containers/Explore/QueryRows.tsx | 7 +-- public/app/core/specs/store.jest.ts | 12 +++++ public/app/core/store.ts | 32 ++++++++++++ 6 files changed, 142 insertions(+), 12 deletions(-) diff --git a/public/app/containers/Explore/Explore.tsx b/public/app/containers/Explore/Explore.tsx index a0bb38a13f1..e4de96dbdf2 100644 --- a/public/app/containers/Explore/Explore.tsx +++ b/public/app/containers/Explore/Explore.tsx @@ -4,6 +4,7 @@ import Select from 'react-select'; import kbn from 'app/core/utils/kbn'; import colors from 'app/core/utils/colors'; +import store from 'app/core/store'; import TimeSeries from 'app/core/time_series2'; import { decodePathComponent } from 'app/core/utils/location_util'; import { parse as parseDate } from 'app/core/utils/datemath'; @@ -16,6 +17,8 @@ import Table from './Table'; import TimePicker, { DEFAULT_RANGE } from './TimePicker'; import { ensureQueries, generateQueryKey, hasQuery } from './utils/query'; +const MAX_HISTORY_ITEMS = 100; + function makeTimeSeriesList(dataList, options) { return dataList.map((seriesData, index) => { const datapoints = seriesData.datapoints || []; @@ -56,6 +59,7 @@ interface IExploreState { datasourceLoading: boolean | null; datasourceMissing: boolean; graphResult: any; + history: any[]; initialDatasource?: string; latency: number; loading: any; @@ -86,6 +90,7 @@ export class Explore extends React.Component { datasourceMissing: false, graphResult: null, initialDatasource: datasource, + history: [], latency: 0, loading: false, logsResult: null, @@ -138,6 +143,7 @@ export class Explore extends React.Component { const supportsGraph = datasource.meta.metrics; const supportsLogs = datasource.meta.logs; const supportsTable = datasource.meta.metrics; + const datasourceId = datasource.meta.id; let datasourceError = null; try { @@ -147,10 +153,14 @@ export class Explore extends React.Component { datasourceError = (error && error.statusText) || error; } + const historyKey = `grafana.explore.history.${datasourceId}`; + const history = store.getObject(historyKey, []); + this.setState( { datasource, datasourceError, + history, supportsGraph, supportsLogs, supportsTable, @@ -269,6 +279,27 @@ export class Explore extends React.Component { } }; + onQuerySuccess(datasourceId: string, queries: any[]): void { + // save queries to history + let { datasource, history } = this.state; + if (datasource.meta.id !== datasourceId) { + // Navigated away, queries did not matter + return; + } + const ts = Date.now(); + queries.forEach(q => { + const { query } = q; + history = [...history, { query, ts }]; + }); + if (history.length > MAX_HISTORY_ITEMS) { + history = history.slice(history.length - MAX_HISTORY_ITEMS); + } + // Combine all queries of a datasource type into one history + const historyKey = `grafana.explore.history.${datasourceId}`; + store.setObject(historyKey, history); + this.setState({ history }); + } + buildQueryOptions(targetOptions: { format: string; instant?: boolean }) { const { datasource, queries, range } = this.state; const resolution = this.el.offsetWidth; @@ -301,6 +332,7 @@ export class Explore extends React.Component { const result = makeTimeSeriesList(res.data, options); const latency = Date.now() - now; this.setState({ latency, loading: false, graphResult: result, requestOptions: options }); + this.onQuerySuccess(datasource.meta.id, queries); } catch (response) { console.error(response); const queryError = response.data ? response.data.error : response; @@ -324,6 +356,7 @@ export class Explore extends React.Component { const tableModel = res.data[0]; const latency = Date.now() - now; this.setState({ latency, loading: false, tableResult: tableModel, requestOptions: options }); + this.onQuerySuccess(datasource.meta.id, queries); } catch (response) { console.error(response); const queryError = response.data ? response.data.error : response; @@ -347,6 +380,7 @@ export class Explore extends React.Component { const logsData = res.data; const latency = Date.now() - now; this.setState({ latency, loading: false, logsResult: logsData, requestOptions: options }); + this.onQuerySuccess(datasource.meta.id, queries); } catch (response) { console.error(response); const queryError = response.data ? response.data.error : response; @@ -367,6 +401,7 @@ export class Explore extends React.Component { datasourceLoading, datasourceMissing, graphResult, + history, latency, loading, logsResult, @@ -405,12 +440,12 @@ export class Explore extends React.Component {
    ) : ( -
    - -
    - )} +
    + )} {!datasourceMissing ? (
    { onExecuteQuery={this.handleSubmit} onRemoveQueryRow={this.handleRemoveQueryRow} /> - {queryError ?
    {queryError}
    : null} + {queryError && !loading ?
    {queryError}
    : null}
    {supportsGraph && showingGraph ? ( ) : null} {supportsTable && showingTable ? ( - +
    ) : null} - {supportsLogs && showingLogs ? : null} + {supportsLogs && showingLogs ? : null} ) : null} diff --git a/public/app/containers/Explore/Graph.tsx b/public/app/containers/Explore/Graph.tsx index a43ddfb2aa5..eeda29b1292 100644 --- a/public/app/containers/Explore/Graph.tsx +++ b/public/app/containers/Explore/Graph.tsx @@ -123,7 +123,14 @@ class Graph extends Component { } render() { - const { data, height } = this.props; + const { data, height, loading } = this.props; + if (!loading && data && data.length === 0) { + return ( +
    +
    The queries returned no time series to graph.
    +
    + ); + } return (
    diff --git a/public/app/containers/Explore/Logs.tsx b/public/app/containers/Explore/Logs.tsx index 10d7827a9a3..ae2d5e2daa6 100644 --- a/public/app/containers/Explore/Logs.tsx +++ b/public/app/containers/Explore/Logs.tsx @@ -5,6 +5,7 @@ import { LogsModel, LogRow } from 'app/core/logs_model'; interface LogsProps { className?: string; data: LogsModel; + loading: boolean; } const EXAMPLE_QUERY = '{job="default/prometheus"}'; diff --git a/public/app/containers/Explore/Table.tsx b/public/app/containers/Explore/Table.tsx index 0856acd5d89..5cf41563704 100644 --- a/public/app/containers/Explore/Table.tsx +++ b/public/app/containers/Explore/Table.tsx @@ -6,6 +6,7 @@ const EMPTY_TABLE = new TableModel(); interface TableProps { className?: string; data: TableModel; + loading: boolean; onClickCell?: (columnKey: string, rowValue: string) => void; } @@ -38,8 +39,24 @@ function Cell(props: SFCCellProps) { export default class Table extends PureComponent { render() { - const { className = '', data, onClickCell } = this.props; - const tableModel = data || EMPTY_TABLE; + const { className = '', data, loading, onClickCell } = this.props; + let tableModel = data || EMPTY_TABLE; + if (!loading && data && data.rows.length === 0) { + return ( +
    + + + + + + + + + + +
    Table
    The queries returned no data for a table.
    + ); + } return ( From 00f04f4ea0d0eab8ab1cc724b5431675e98d8d91 Mon Sep 17 00:00:00 2001 From: David Kaltschmidt Date: Sat, 4 Aug 2018 11:47:04 +0200 Subject: [PATCH 318/786] Add clear button to Explore - Clear All button to clear all queries and results - moved result viewer buttons below query rows to make it more clear that they govern result options --- public/app/containers/Explore/Explore.tsx | 50 +++++++++++++++-------- public/app/containers/Explore/Graph.tsx | 3 +- public/sass/pages/_explore.scss | 8 ++++ 3 files changed, 43 insertions(+), 18 deletions(-) diff --git a/public/app/containers/Explore/Explore.tsx b/public/app/containers/Explore/Explore.tsx index 53c43782ad6..772617dd7c1 100644 --- a/public/app/containers/Explore/Explore.tsx +++ b/public/app/containers/Explore/Explore.tsx @@ -267,6 +267,15 @@ export class Explore extends React.Component { } }; + onClickClear = () => { + this.setState({ + graphResult: null, + logsResult: null, + queries: ensureQueries(), + tableResult: null, + }); + }; + onClickTableCell = (columnKey: string, rowValue: string) => { const { datasource, queries } = this.state; if (datasource && datasource.modifyQuery) { @@ -466,24 +475,12 @@ export class Explore extends React.Component { ) : null} -
    - {supportsGraph ? ( - - ) : null} - {supportsTable ? ( - - ) : null} - {supportsLogs ? ( - - ) : null} -
    +
    + +
    + ) : null} + {supportsTable ? ( + + ) : null} + {supportsLogs ? ( + + ) : null} +
    +
    {supportsGraph && showingGraph ? ( { draw() { const { data, options: userOptions } = this.props; + const $el = $(`#${this.props.id}`); if (!data) { + $el.empty(); return; } const series = data.map((ts: TimeSeries) => ({ @@ -93,7 +95,6 @@ class Graph extends Component { data: ts.getFlotPairs('null'), })); - const $el = $(`#${this.props.id}`); const ticks = $el.width() / 100; let { from, to } = userOptions.range; if (!moment.isMoment(from)) { diff --git a/public/sass/pages/_explore.scss b/public/sass/pages/_explore.scss index 59b8b62f349..52ddbc03636 100644 --- a/public/sass/pages/_explore.scss +++ b/public/sass/pages/_explore.scss @@ -47,6 +47,14 @@ background-color: $btn-active-bg; } + .navbar-button--no-icon { + line-height: 18px; + } + + .result-options { + margin-top: 2 * $panel-margin; + } + .elapsed-time { position: absolute; left: 0; From 307248f713d00b889325b353ec9ba47f1c87f914 Mon Sep 17 00:00:00 2001 From: David Kaltschmidt Date: Sat, 4 Aug 2018 11:58:54 +0200 Subject: [PATCH 319/786] Add clear row button - clears the content of a query row --- public/app/containers/Explore/Explore.tsx | 136 ++++++++++---------- public/app/containers/Explore/QueryRows.tsx | 26 ++-- public/sass/pages/_explore.scss | 2 +- 3 files changed, 87 insertions(+), 77 deletions(-) diff --git a/public/app/containers/Explore/Explore.tsx b/public/app/containers/Explore/Explore.tsx index 772617dd7c1..b21a78ed8ab 100644 --- a/public/app/containers/Explore/Explore.tsx +++ b/public/app/containers/Explore/Explore.tsx @@ -166,7 +166,7 @@ export class Explore extends React.Component { supportsTable, datasourceLoading: false, }, - () => datasourceError === null && this.handleSubmit() + () => datasourceError === null && this.onSubmit() ); } @@ -174,7 +174,7 @@ export class Explore extends React.Component { this.el = el; }; - handleAddQueryRow = index => { + onAddQueryRow = index => { const { queries } = this.state; const nextQueries = [ ...queries.slice(0, index + 1), @@ -184,7 +184,7 @@ export class Explore extends React.Component { this.setState({ queries: nextQueries }); }; - handleChangeDatasource = async option => { + onChangeDatasource = async option => { this.setState({ datasource: null, datasourceError: null, @@ -197,10 +197,10 @@ export class Explore extends React.Component { this.setDatasource(datasource); }; - handleChangeQuery = (value, index) => { + onChangeQuery = (value: string, index: number, override?: boolean) => { const { queries } = this.state; const prevQuery = queries[index]; - const edited = prevQuery.query !== value; + const edited = override ? false : prevQuery.query !== value; const nextQuery = { ...queries[index], edited, @@ -211,60 +211,12 @@ export class Explore extends React.Component { this.setState({ queries: nextQueries }); }; - handleChangeTime = nextRange => { + onChangeTime = nextRange => { const range = { from: nextRange.from, to: nextRange.to, }; - this.setState({ range }, () => this.handleSubmit()); - }; - - handleClickCloseSplit = () => { - const { onChangeSplit } = this.props; - if (onChangeSplit) { - onChangeSplit(false); - } - }; - - handleClickGraphButton = () => { - this.setState(state => ({ showingGraph: !state.showingGraph })); - }; - - handleClickLogsButton = () => { - this.setState(state => ({ showingLogs: !state.showingLogs })); - }; - - handleClickSplit = () => { - const { onChangeSplit } = this.props; - if (onChangeSplit) { - onChangeSplit(true, this.state); - } - }; - - handleClickTableButton = () => { - this.setState(state => ({ showingTable: !state.showingTable })); - }; - - handleRemoveQueryRow = index => { - const { queries } = this.state; - if (queries.length <= 1) { - return; - } - const nextQueries = [...queries.slice(0, index), ...queries.slice(index + 1)]; - this.setState({ queries: nextQueries }, () => this.handleSubmit()); - }; - - handleSubmit = () => { - const { showingLogs, showingGraph, showingTable, supportsGraph, supportsLogs, supportsTable } = this.state; - if (showingTable && supportsTable) { - this.runTableQuery(); - } - if (showingGraph && supportsGraph) { - this.runGraphQuery(); - } - if (showingLogs && supportsLogs) { - this.runLogsQuery(); - } + this.setState({ range }, () => this.onSubmit()); }; onClickClear = () => { @@ -276,6 +228,32 @@ export class Explore extends React.Component { }); }; + onClickCloseSplit = () => { + const { onChangeSplit } = this.props; + if (onChangeSplit) { + onChangeSplit(false); + } + }; + + onClickGraphButton = () => { + this.setState(state => ({ showingGraph: !state.showingGraph })); + }; + + onClickLogsButton = () => { + this.setState(state => ({ showingLogs: !state.showingLogs })); + }; + + onClickSplit = () => { + const { onChangeSplit } = this.props; + if (onChangeSplit) { + onChangeSplit(true, this.state); + } + }; + + onClickTableButton = () => { + this.setState(state => ({ showingTable: !state.showingTable })); + }; + onClickTableCell = (columnKey: string, rowValue: string) => { const { datasource, queries } = this.state; if (datasource && datasource.modifyQuery) { @@ -284,7 +262,29 @@ export class Explore extends React.Component { edited: false, query: datasource.modifyQuery(q.query, { addFilter: { key: columnKey, value: rowValue } }), })); - this.setState({ queries: nextQueries }, () => this.handleSubmit()); + this.setState({ queries: nextQueries }, () => this.onSubmit()); + } + }; + + onRemoveQueryRow = index => { + const { queries } = this.state; + if (queries.length <= 1) { + return; + } + const nextQueries = [...queries.slice(0, index), ...queries.slice(index + 1)]; + this.setState({ queries: nextQueries }, () => this.onSubmit()); + }; + + onSubmit = () => { + const { showingLogs, showingGraph, showingTable, supportsGraph, supportsLogs, supportsTable } = this.state; + if (showingTable && supportsTable) { + this.runTableQuery(); + } + if (showingGraph && supportsGraph) { + this.runGraphQuery(); + } + if (showingLogs && supportsLogs) { + this.runLogsQuery(); } }; @@ -450,7 +450,7 @@ export class Explore extends React.Component { ) : (
    -
    @@ -460,7 +460,7 @@ export class Explore extends React.Component {
    {row.map((value, j) => ( - + ))} ))} diff --git a/public/sass/_grafana.scss b/public/sass/_grafana.scss index ec7103cba95..be3a3b90f78 100644 --- a/public/sass/_grafana.scss +++ b/public/sass/_grafana.scss @@ -1,7 +1,7 @@ // vendor @import '../vendor/css/timepicker.css'; @import '../vendor/css/spectrum.css'; -@import '../vendor/css/rc-cascader.css'; +@import '../vendor/css/rc-cascader.scss'; // MIXINS @import 'mixins/mixins'; diff --git a/public/vendor/css/rc-cascader.css b/public/vendor/css/rc-cascader.scss similarity index 88% rename from public/vendor/css/rc-cascader.css rename to public/vendor/css/rc-cascader.scss index 968c1fc770f..5cfaaf4961a 100644 --- a/public/vendor/css/rc-cascader.css +++ b/public/vendor/css/rc-cascader.scss @@ -4,11 +4,11 @@ .rc-cascader-menus { font-size: 12px; overflow: hidden; - background: #fff; + background: $panel-bg; position: absolute; - border: 1px solid #d9d9d9; - border-radius: 6px; - box-shadow: 0 0 4px rgba(0, 0, 0, 0.17); + border: $panel-border; + border-radius: $border-radius; + box-shadow: $typeahead-shadow; white-space: nowrap; } .rc-cascader-menus-hidden { @@ -57,7 +57,7 @@ list-style: none; margin: 0; padding: 0; - border-right: 1px solid #e9e9e9; + border-right: $panel-border; overflow: auto; } .rc-cascader-menu:last-child { @@ -75,11 +75,11 @@ position: relative; } .rc-cascader-menu-item:hover { - background: #eaf8fe; + background: $typeahead-selected-bg; } .rc-cascader-menu-item-disabled { cursor: not-allowed; - color: #ccc; + color: $text-color-weak; } .rc-cascader-menu-item-disabled:hover { background: transparent; @@ -88,14 +88,16 @@ position: absolute; right: 12px; content: 'loading'; - color: #aaa; + color: $text-color-weak; font-style: italic; } .rc-cascader-menu-item-active { - background: #d5f1fd; + color: $typeahead-selected-color; + background: $typeahead-selected-bg; } .rc-cascader-menu-item-active:hover { - background: #d5f1fd; + color: $typeahead-selected-color; + background: $typeahead-selected-bg; } .rc-cascader-menu-item-expand { position: relative; @@ -103,7 +105,7 @@ .rc-cascader-menu-item-expand:after { content: '>'; font-size: 12px; - color: #999; + color: $text-color-weak; position: absolute; right: 16px; line-height: 32px; From eb1b9405b2f8b410ff28479abe4192de365b9a79 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Tue, 7 Aug 2018 17:56:02 +0200 Subject: [PATCH 325/786] return proper payload from api when updating datasource --- pkg/api/datasources.go | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/pkg/api/datasources.go b/pkg/api/datasources.go index 6ffefea991a..23dbb221d71 100644 --- a/pkg/api/datasources.go +++ b/pkg/api/datasources.go @@ -158,12 +158,26 @@ func UpdateDataSource(c *m.ReqContext, cmd m.UpdateDataSourceCommand) Response { } return Error(500, "Failed to update datasource", err) } - ds := convertModelToDtos(cmd.Result) + + query := m.GetDataSourceByIdQuery{ + Id: cmd.Id, + OrgId: c.OrgId, + } + + if err := bus.Dispatch(&query); err != nil { + if err == m.ErrDataSourceNotFound { + return Error(404, "Data source not found", nil) + } + return Error(500, "Failed to query datasources", err) + } + + dtos := convertModelToDtos(query.Result) + return JSON(200, util.DynMap{ "message": "Datasource updated", "id": cmd.Id, "name": cmd.Name, - "datasource": ds, + "datasource": dtos, }) } From ee7602ec1fd8e1303dc12a3c7f6fc105228e2893 Mon Sep 17 00:00:00 2001 From: Sven Klemm Date: Tue, 7 Aug 2018 21:01:41 +0200 Subject: [PATCH 326/786] change fillmode from last to previous --- docs/sources/features/datasources/mssql.md | 2 +- docs/sources/features/datasources/mysql.md | 2 +- docs/sources/features/datasources/postgres.md | 3 +-- pkg/tsdb/mssql/macros.go | 4 ++-- pkg/tsdb/mssql/macros_test.go | 6 +++--- pkg/tsdb/mysql/macros.go | 4 ++-- pkg/tsdb/mysql/mysql_test.go | 4 ++-- pkg/tsdb/postgres/macros.go | 4 ++-- pkg/tsdb/postgres/postgres_test.go | 4 ++-- pkg/tsdb/sql_engine.go | 10 +++++----- 10 files changed, 21 insertions(+), 22 deletions(-) diff --git a/docs/sources/features/datasources/mssql.md b/docs/sources/features/datasources/mssql.md index 9a149df120d..caaf5a6b321 100644 --- a/docs/sources/features/datasources/mssql.md +++ b/docs/sources/features/datasources/mssql.md @@ -83,7 +83,7 @@ Macro example | Description *$__timeGroup(dateColumn,'5m'[, fillvalue])* | Will be replaced by an expression usable in GROUP BY clause. Providing a *fillValue* of *NULL* or *floating value* will automatically fill empty series in timerange with that value.
    For example, *CAST(ROUND(DATEDIFF(second, '1970-01-01', time_column)/300.0, 0) as bigint)\*300*. *$__timeGroup(dateColumn,'5m', 0)* | Same as above but with a fill parameter so missing points in that series will be added by grafana and 0 will be used as value. *$__timeGroup(dateColumn,'5m', NULL)* | Same as above but NULL will be used as value for missing points. -*$__timeGroup(dateColumn,'5m', last)* | Same as above but the last seen value in that series will be used as fill value if no value has been seen yet NULL will be used (only available in Grafana 5.3+). +*$__timeGroup(dateColumn,'5m', previous)* | Same as above but the previous value in that series will be used as fill value if no value has been seen yet NULL will be used (only available in Grafana 5.3+). *$__timeGroupAlias(dateColumn,'5m')* | Will be replaced identical to $__timeGroup but with an added column alias (only available in Grafana 5.3+). *$__unixEpochFilter(dateColumn)* | Will be replaced by a time range filter using the specified column name with times represented as unix timestamp. For example, *dateColumn > 1494410783 AND dateColumn < 1494497183* *$__unixEpochFrom()* | Will be replaced by the start of the currently active time selection as unix timestamp. For example, *1494410783* diff --git a/docs/sources/features/datasources/mysql.md b/docs/sources/features/datasources/mysql.md index 4f4efb6e29a..cdb78deed35 100644 --- a/docs/sources/features/datasources/mysql.md +++ b/docs/sources/features/datasources/mysql.md @@ -66,7 +66,7 @@ Macro example | Description *$__timeGroup(dateColumn,'5m')* | Will be replaced by an expression usable in GROUP BY clause. For example, *cast(cast(UNIX_TIMESTAMP(dateColumn)/(300) as signed)*300 as signed),* *$__timeGroup(dateColumn,'5m', 0)* | Same as above but with a fill parameter so missing points in that series will be added by grafana and 0 will be used as value. *$__timeGroup(dateColumn,'5m', NULL)* | Same as above but NULL will be used as value for missing points. -*$__timeGroup(dateColumn,'5m', last)* | Same as above but the last seen value in that series will be used as fill value if no value has been seen yet NULL will be used (only available in Grafana 5.3+). +*$__timeGroup(dateColumn,'5m', previous)* | Same as above but the previous value in that series will be used as fill value if no value has been seen yet NULL will be used (only available in Grafana 5.3+). *$__timeGroupAlias(dateColumn,'5m')* | Will be replaced identical to $__timeGroup but with an added column alias (only available in Grafana 5.3+). *$__unixEpochFilter(dateColumn)* | Will be replaced by a time range filter using the specified column name with times represented as unix timestamp. For example, *dateColumn > 1494410783 AND dateColumn < 1494497183* *$__unixEpochFrom()* | Will be replaced by the start of the currently active time selection as unix timestamp. For example, *1494410783* diff --git a/docs/sources/features/datasources/postgres.md b/docs/sources/features/datasources/postgres.md index f2b54d3f0ce..2be2db0837b 100644 --- a/docs/sources/features/datasources/postgres.md +++ b/docs/sources/features/datasources/postgres.md @@ -63,8 +63,7 @@ Macro example | Description *$__timeGroup(dateColumn,'5m')* | Will be replaced by an expression usable in GROUP BY clause. For example, *(extract(epoch from dateColumn)/300)::bigint*300* *$__timeGroup(dateColumn,'5m', 0)* | Same as above but with a fill parameter so missing points in that series will be added by grafana and 0 will be used as value. *$__timeGroup(dateColumn,'5m', NULL)* | Same as above but NULL will be used as value for missing points. -*$__timeGroup(dateColumn,'5m', last)* | Same as above but the last seen value in that series will be used as fill value if no value has been seen yet NULL will be used. -*$__timeGroup(dateColumn,'5m', last)* | Same as above but the last seen value in that series will be used as fill value if no value has been seen yet NULL will be used (only available in Grafana 5.3+). +*$__timeGroup(dateColumn,'5m', previous)* | Same as above but the previous value in that series will be used as fill value if no value has been seen yet NULL will be used (only available in Grafana 5.3+). *$__timeGroupAlias(dateColumn,'5m')* | Will be replaced identical to $__timeGroup but with an added column alias (only available in Grafana 5.3+). *$__unixEpochFilter(dateColumn)* | Will be replaced by a time range filter using the specified column name with times represented as unix timestamp. For example, *dateColumn >= 1494410783 AND dateColumn <= 1494497183* *$__unixEpochFrom()* | Will be replaced by the start of the currently active time selection as unix timestamp. For example, *1494410783* diff --git a/pkg/tsdb/mssql/macros.go b/pkg/tsdb/mssql/macros.go index 57a37d618e0..42e47ce6d3c 100644 --- a/pkg/tsdb/mssql/macros.go +++ b/pkg/tsdb/mssql/macros.go @@ -102,8 +102,8 @@ func (m *msSqlMacroEngine) evaluateMacro(name string, args []string) (string, er switch args[2] { case "NULL": m.query.Model.Set("fillMode", "null") - case "last": - m.query.Model.Set("fillMode", "last") + case "previous": + m.query.Model.Set("fillMode", "previous") default: m.query.Model.Set("fillMode", "value") floatVal, err := strconv.ParseFloat(args[2], 64) diff --git a/pkg/tsdb/mssql/macros_test.go b/pkg/tsdb/mssql/macros_test.go index b808666d967..8362ae05aa6 100644 --- a/pkg/tsdb/mssql/macros_test.go +++ b/pkg/tsdb/mssql/macros_test.go @@ -85,8 +85,8 @@ func TestMacroEngine(t *testing.T) { So(fillInterval, ShouldEqual, 5*time.Minute.Seconds()) }) - Convey("interpolate __timeGroup function with fill (value = last)", func() { - _, err := engine.Interpolate(query, timeRange, "GROUP BY $__timeGroup(time_column,'5m', last)") + Convey("interpolate __timeGroup function with fill (value = previous)", func() { + _, err := engine.Interpolate(query, timeRange, "GROUP BY $__timeGroup(time_column,'5m', previous)") fill := query.Model.Get("fill").MustBool() fillMode := query.Model.Get("fillMode").MustString() @@ -94,7 +94,7 @@ func TestMacroEngine(t *testing.T) { So(err, ShouldBeNil) So(fill, ShouldBeTrue) - So(fillMode, ShouldEqual, "last") + So(fillMode, ShouldEqual, "previous") So(fillInterval, ShouldEqual, 5*time.Minute.Seconds()) }) diff --git a/pkg/tsdb/mysql/macros.go b/pkg/tsdb/mysql/macros.go index bebf4b396bb..905d424f29a 100644 --- a/pkg/tsdb/mysql/macros.go +++ b/pkg/tsdb/mysql/macros.go @@ -97,8 +97,8 @@ func (m *mySqlMacroEngine) evaluateMacro(name string, args []string) (string, er switch args[2] { case "NULL": m.query.Model.Set("fillMode", "null") - case "last": - m.query.Model.Set("fillMode", "last") + case "previous": + m.query.Model.Set("fillMode", "previous") default: m.query.Model.Set("fillMode", "value") floatVal, err := strconv.ParseFloat(args[2], 64) diff --git a/pkg/tsdb/mysql/mysql_test.go b/pkg/tsdb/mysql/mysql_test.go index fe262a3f758..ca6df8e360e 100644 --- a/pkg/tsdb/mysql/mysql_test.go +++ b/pkg/tsdb/mysql/mysql_test.go @@ -321,12 +321,12 @@ func TestMySQL(t *testing.T) { So(points[3][0].Float64, ShouldEqual, 1.5) }) - Convey("When doing a metric query using timeGroup with last fill enabled", func() { + Convey("When doing a metric query using timeGroup with previous fill enabled", func() { query := &tsdb.TsdbQuery{ Queries: []*tsdb.Query{ { Model: simplejson.NewFromAny(map[string]interface{}{ - "rawSql": "SELECT $__timeGroup(time, '5m', last) as time_sec, avg(value) as value FROM metric GROUP BY 1 ORDER BY 1", + "rawSql": "SELECT $__timeGroup(time, '5m', previous) as time_sec, avg(value) as value FROM metric GROUP BY 1 ORDER BY 1", "format": "time_series", }), RefId: "A", diff --git a/pkg/tsdb/postgres/macros.go b/pkg/tsdb/postgres/macros.go index 3ab21ea0c6e..aebdc55d1d7 100644 --- a/pkg/tsdb/postgres/macros.go +++ b/pkg/tsdb/postgres/macros.go @@ -119,8 +119,8 @@ func (m *postgresMacroEngine) evaluateMacro(name string, args []string) (string, switch args[2] { case "NULL": m.query.Model.Set("fillMode", "null") - case "last": - m.query.Model.Set("fillMode", "last") + case "previous": + m.query.Model.Set("fillMode", "previous") default: m.query.Model.Set("fillMode", "value") floatVal, err := strconv.ParseFloat(args[2], 64) diff --git a/pkg/tsdb/postgres/postgres_test.go b/pkg/tsdb/postgres/postgres_test.go index ac0964e912c..9e363529df1 100644 --- a/pkg/tsdb/postgres/postgres_test.go +++ b/pkg/tsdb/postgres/postgres_test.go @@ -303,12 +303,12 @@ func TestPostgres(t *testing.T) { }) }) - Convey("When doing a metric query using timeGroup with last fill enabled", func() { + Convey("When doing a metric query using timeGroup with previous fill enabled", func() { query := &tsdb.TsdbQuery{ Queries: []*tsdb.Query{ { Model: simplejson.NewFromAny(map[string]interface{}{ - "rawSql": "SELECT $__timeGroup(time, '5m', last), avg(value) as value FROM metric GROUP BY 1 ORDER BY 1", + "rawSql": "SELECT $__timeGroup(time, '5m', previous), avg(value) as value FROM metric GROUP BY 1 ORDER BY 1", "format": "time_series", }), RefId: "A", diff --git a/pkg/tsdb/sql_engine.go b/pkg/tsdb/sql_engine.go index f2f8b17db5f..cbf6d6b4d60 100644 --- a/pkg/tsdb/sql_engine.go +++ b/pkg/tsdb/sql_engine.go @@ -274,14 +274,14 @@ func (e *sqlQueryEndpoint) transformToTimeSeries(query *Query, rows *core.Rows, fillMissing := query.Model.Get("fill").MustBool(false) var fillInterval float64 fillValue := null.Float{} - fillLast := false + fillPrevious := false if fillMissing { fillInterval = query.Model.Get("fillInterval").MustFloat64() * 1000 switch query.Model.Get("fillMode").MustString() { case "null": - case "last": - fillLast = true + case "previous": + fillPrevious = true case "value": fillValue.Float64 = query.Model.Get("fillValue").MustFloat64() fillValue.Valid = true @@ -358,7 +358,7 @@ func (e *sqlQueryEndpoint) transformToTimeSeries(query *Query, rows *core.Rows, intervalStart = series.Points[len(series.Points)-1][1].Float64 + fillInterval } - if fillLast { + if fillPrevious { if len(series.Points) > 0 { fillValue = series.Points[len(series.Points)-1][0] } else { @@ -391,7 +391,7 @@ func (e *sqlQueryEndpoint) transformToTimeSeries(query *Query, rows *core.Rows, intervalStart := series.Points[len(series.Points)-1][1].Float64 intervalEnd := float64(tsdbQuery.TimeRange.MustGetTo().UnixNano() / 1e6) - if fillLast { + if fillPrevious { if len(series.Points) > 0 { fillValue = series.Points[len(series.Points)-1][0] } else { From 52c7edf2f41e4c3479b39e401b4e1778c461f581 Mon Sep 17 00:00:00 2001 From: Sven Klemm Date: Tue, 7 Aug 2018 21:11:51 +0200 Subject: [PATCH 327/786] rename last fillmode to previous --- public/app/plugins/datasource/mssql/partials/query.editor.html | 2 +- public/app/plugins/datasource/mysql/partials/query.editor.html | 2 +- .../app/plugins/datasource/postgres/partials/query.editor.html | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/public/app/plugins/datasource/mssql/partials/query.editor.html b/public/app/plugins/datasource/mssql/partials/query.editor.html index e873d60ebbf..7888e36a24c 100644 --- a/public/app/plugins/datasource/mssql/partials/query.editor.html +++ b/public/app/plugins/datasource/mssql/partials/query.editor.html @@ -55,7 +55,7 @@ Macros: - $__unixEpochFilter(column) -> column >= 1492750877 AND column <= 1492750877 - $__timeGroup(column, '5m'[, fillvalue]) -> CAST(ROUND(DATEDIFF(second, '1970-01-01', column)/300.0, 0) as bigint)*300. by setting fillvalue grafana will fill in missing values according to the interval - fillvalue can be either a literal value, NULL or last; last will fill in the last seen value or NULL if none has been seen yet + fillvalue can be either a literal value, NULL or previous; previous will fill in the previous seen value or NULL if none has been seen yet - $__timeGroupAlias(column, '5m'[, fillvalue]) -> CAST(ROUND(DATEDIFF(second, '1970-01-01', column)/300.0, 0) as bigint)*300 AS [time] Example of group by and order by with $__timeGroup: diff --git a/public/app/plugins/datasource/mysql/partials/query.editor.html b/public/app/plugins/datasource/mysql/partials/query.editor.html index 664481ec8dc..7c799eec21b 100644 --- a/public/app/plugins/datasource/mysql/partials/query.editor.html +++ b/public/app/plugins/datasource/mysql/partials/query.editor.html @@ -55,7 +55,7 @@ Macros: - $__unixEpochFilter(column) -> time_unix_epoch > 1492750877 AND time_unix_epoch < 1492750877 - $__timeGroup(column,'5m'[, fillvalue]) -> cast(cast(UNIX_TIMESTAMP(column)/(300) as signed)*300 as signed) by setting fillvalue grafana will fill in missing values according to the interval - fillvalue can be either a literal value, NULL or last; last will fill in the last seen value or NULL if none has been seen yet + fillvalue can be either a literal value, NULL or previous; previous will fill in the previous seen value or NULL if none has been seen yet - $__timeGroupAlias(column,'5m') -> cast(cast(UNIX_TIMESTAMP(column)/(300) as signed)*300 as signed) AS "time" Example of group by and order by with $__timeGroup: diff --git a/public/app/plugins/datasource/postgres/partials/query.editor.html b/public/app/plugins/datasource/postgres/partials/query.editor.html index c455c0ebaf9..20353b81ba2 100644 --- a/public/app/plugins/datasource/postgres/partials/query.editor.html +++ b/public/app/plugins/datasource/postgres/partials/query.editor.html @@ -55,7 +55,7 @@ Macros: - $__unixEpochFilter(column) -> column >= 1492750877 AND column <= 1492750877 - $__timeGroup(column,'5m'[, fillvalue]) -> (extract(epoch from column)/300)::bigint*300 by setting fillvalue grafana will fill in missing values according to the interval - fillvalue can be either a literal value, NULL or last; last will fill in the last seen value or NULL if none has been seen yet + fillvalue can be either a literal value, NULL or previous; previous will fill in the previous seen value or NULL if none has been seen yet - $__timeGroupAlias(column,'5m') -> (extract(epoch from column)/300)::bigint*300 AS "time" Example of group by and order by with $__timeGroup: From a156b6ee06a4b0610430afb254c23242154f1452 Mon Sep 17 00:00:00 2001 From: Ben de Luca Date: Tue, 7 Aug 2018 22:32:02 +0200 Subject: [PATCH 328/786] fix missing * The missing * causes the text to be in the box to be displayed incorrectly. --- docs/sources/features/datasources/elasticsearch.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/sources/features/datasources/elasticsearch.md b/docs/sources/features/datasources/elasticsearch.md index 31ce78f0bfe..d29327cf480 100644 --- a/docs/sources/features/datasources/elasticsearch.md +++ b/docs/sources/features/datasources/elasticsearch.md @@ -115,7 +115,7 @@ The Elasticsearch data source supports two types of queries you can use in the * Query | Description ------------ | ------------- -*{"find": "fields", "type": "keyword"} | Returns a list of field names with the index type `keyword`. +*{"find": "fields", "type": "keyword"}* | Returns a list of field names with the index type `keyword`. *{"find": "terms", "field": "@hostname", "size": 1000}* | Returns a list of values for a field using term aggregation. Query will user current dashboard time range as time range for query. *{"find": "terms", "field": "@hostname", "query": ''}* | Returns a list of values for a field using term aggregation & and a specified lucene query filter. Query will use current dashboard time range as time range for query. From e8dfbe94b1e1d6832dfb3acd11dae8b01a8fa6d3 Mon Sep 17 00:00:00 2001 From: tariq1890 Date: Sun, 5 Aug 2018 13:54:06 -0700 Subject: [PATCH 329/786] Fixing bug in url query reader and added test cases --- pkg/util/url.go | 2 +- pkg/util/url_test.go | 27 +++++++++++++++++++++++++++ pkg/util/validation_test.go | 22 ++++++++++++++++++++++ 3 files changed, 50 insertions(+), 1 deletion(-) create mode 100644 pkg/util/validation_test.go diff --git a/pkg/util/url.go b/pkg/util/url.go index c82dcef67c5..fad2d79a6d0 100644 --- a/pkg/util/url.go +++ b/pkg/util/url.go @@ -10,7 +10,7 @@ type UrlQueryReader struct { } func NewUrlQueryReader(urlInfo *url.URL) (*UrlQueryReader, error) { - u, err := url.ParseQuery(urlInfo.String()) + u, err := url.ParseQuery(urlInfo.RawQuery) if err != nil { return nil, err } diff --git a/pkg/util/url_test.go b/pkg/util/url_test.go index 4dd221b9e0b..ee29956f60d 100644 --- a/pkg/util/url_test.go +++ b/pkg/util/url_test.go @@ -4,6 +4,7 @@ import ( "testing" . "github.com/smartystreets/goconvey/convey" + "net/url" ) func TestUrl(t *testing.T) { @@ -43,4 +44,30 @@ func TestUrl(t *testing.T) { So(result, ShouldEqual, "http://localhost:8080/api/") }) + + Convey("When joining two urls where lefthand side has a trailing slash and righthand side has preceding slash", t, func() { + result := JoinUrlFragments("http://localhost:8080/", "/api/") + + So(result, ShouldEqual, "http://localhost:8080/api/") + }) +} + +func TestNewUrlQueryReader(t *testing.T) { + u, _ := url.Parse("http://www.abc.com/foo?bar=baz&bar2=baz2") + uqr, _ := NewUrlQueryReader(u) + + Convey("when trying to retrieve the first query value", t, func() { + result := uqr.Get("bar", "foodef") + So(result, ShouldEqual, "baz") + }) + + Convey("when trying to retrieve the second query value", t, func() { + result := uqr.Get("bar2", "foodef") + So(result, ShouldEqual, "baz2") + }) + + Convey("when trying to retrieve from a non-existent key, the default value is returned", t, func() { + result := uqr.Get("bar3", "foodef") + So(result, ShouldEqual, "foodef") + }) } diff --git a/pkg/util/validation_test.go b/pkg/util/validation_test.go new file mode 100644 index 00000000000..124da1b744b --- /dev/null +++ b/pkg/util/validation_test.go @@ -0,0 +1,22 @@ +package util + +import ( + "testing" + + . "github.com/smartystreets/goconvey/convey" +) + +func TestIsEmail(t *testing.T) { + + Convey("When validating a string that is a valid email", t, func() { + result := IsEmail("abc@def.com") + + So(result, ShouldEqual, true) + }) + + Convey("When validating a string that is not a valid email", t, func() { + result := IsEmail("abcdef.com") + + So(result, ShouldEqual, false) + }) +} From a6a29f0b2071619ee9a64029542cc27a6b125367 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Wed, 8 Aug 2018 09:13:44 +0200 Subject: [PATCH 330/786] changelog: add notes about closing #11270 [skip ci] --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b9d0670c717..4fa417be5f6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -37,6 +37,7 @@ * **Units**: Change units to include characters for power of 2 and 3 [#12744](https://github.com/grafana/grafana/pull/12744), thx [@Worty](https://github.com/Worty) * **Graph**: Option to hide series from tooltip [#3341](https://github.com/grafana/grafana/issues/3341), thx [@mtanda](https://github.com/mtanda) * **UI**: Fix iOS home screen "app" icon and Windows 10 app experience [#12752](https://github.com/grafana/grafana/issues/12752), thx [@andig](https://github.com/andig) +* **Datasource**: Fix UI issue with secret fields after updating datasource [#11270](https://github.com/grafana/grafana/issues/11270) ### Breaking changes From b0ddc15e1ab7f28c6924e3f8448eea2561fcdb45 Mon Sep 17 00:00:00 2001 From: Patrick O'Carroll Date: Wed, 8 Aug 2018 09:23:36 +0200 Subject: [PATCH 331/786] team list for profile page + mock teams --- public/app/features/org/partials/profile.html | 4 ++-- public/app/features/org/profile_ctrl.ts | 7 +++---- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/public/app/features/org/partials/profile.html b/public/app/features/org/partials/profile.html index 96540911290..5cbb21f488a 100644 --- a/public/app/features/org/partials/profile.html +++ b/public/app/features/org/partials/profile.html @@ -32,13 +32,13 @@
    - + - +
    NameEmailMembers
    {{team.name}}{{team.email}}{{team.members}}
    diff --git a/public/app/features/org/profile_ctrl.ts b/public/app/features/org/profile_ctrl.ts index 1ac950699be..361dfa9e52f 100644 --- a/public/app/features/org/profile_ctrl.ts +++ b/public/app/features/org/profile_ctrl.ts @@ -28,12 +28,11 @@ export class ProfileCtrl { } getUserTeams() { - console.log(this.backendSrv.get('/api/teams')); this.backendSrv.get('/api/user').then(teams => { this.user.teams = [ - { name: 'Backend', email: 'backend@grafana.com', members: 2 }, - { name: 'Frontend', email: 'frontend@grafana.com', members: 2 }, - { name: 'Ops', email: 'ops@grafana.com', members: 2 }, + { name: 'Backend', email: 'backend@grafana.com', members: 5 }, + { name: 'Frontend', email: 'frontend@grafana.com', members: 4 }, + { name: 'Ops', email: 'ops@grafana.com', members: 6 }, ]; this.showTeamsList = this.user.teams.length > 1; }); From 9938835dde3be364b549e4ace3eea1c044256f2d Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Wed, 8 Aug 2018 09:47:45 +0200 Subject: [PATCH 332/786] devenv: update sql dashboards --- .../datasource_tests_mssql_unittest.json | 244 +++++++++++++++--- .../datasource_tests_mysql_unittest.json | 240 ++++++++++++++--- .../datasource_tests_postgres_unittest.json | 243 ++++++++++++++--- 3 files changed, 612 insertions(+), 115 deletions(-) diff --git a/devenv/dev-dashboards/datasource_tests_mssql_unittest.json b/devenv/dev-dashboards/datasource_tests_mssql_unittest.json index 80d3e1a5889..0d291f01a09 100644 --- a/devenv/dev-dashboards/datasource_tests_mssql_unittest.json +++ b/devenv/dev-dashboards/datasource_tests_mssql_unittest.json @@ -64,7 +64,7 @@ "editable": true, "gnetId": null, "graphTooltip": 0, - "iteration": 1532949769359, + "iteration": 1533713720618, "links": [], "panels": [ { @@ -338,8 +338,8 @@ "datasource": "gdev-mssql-ds-tests", "fill": 2, "gridPos": { - "h": 9, - "w": 8, + "h": 6, + "w": 6, "x": 0, "y": 7 }, @@ -421,9 +421,9 @@ "datasource": "gdev-mssql-ds-tests", "fill": 2, "gridPos": { - "h": 9, - "w": 8, - "x": 8, + "h": 6, + "w": 6, + "x": 6, "y": 7 }, "id": 9, @@ -504,9 +504,9 @@ "datasource": "gdev-mssql-ds-tests", "fill": 2, "gridPos": { - "h": 9, - "w": 8, - "x": 16, + "h": 6, + "w": 6, + "x": 12, "y": 7 }, "id": 10, @@ -579,6 +579,89 @@ "alignLevel": null } }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "gdev-mssql-ds-tests", + "fill": 2, + "gridPos": { + "h": 6, + "w": 6, + "x": 18, + "y": 7 + }, + "id": 36, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 2, + "links": [], + "nullPointMode": "null as zero", + "percentage": false, + "pointradius": 3, + "points": true, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": true, + "targets": [ + { + "alias": "", + "format": "time_series", + "rawSql": "SELECT $__timeGroupAlias(time, '5m', previous), avg(value) as value FROM metric WHERE $__timeFilter(time) GROUP BY $__timeGroup(time, '5m') ORDER BY 1", + "refId": "A" + } + ], + "thresholds": [], + "timeFrom": null, + "timeShift": null, + "title": "timeGroup macro 5m with fill(previous) and null as zero", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": "0", + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, { "aliasColors": {}, "bars": true, @@ -587,10 +670,10 @@ "datasource": "gdev-mssql-ds-tests", "fill": 2, "gridPos": { - "h": 9, - "w": 8, + "h": 6, + "w": 6, "x": 0, - "y": 16 + "y": 13 }, "id": 16, "legend": { @@ -670,10 +753,10 @@ "datasource": "gdev-mssql-ds-tests", "fill": 2, "gridPos": { - "h": 9, - "w": 8, - "x": 8, - "y": 16 + "h": 6, + "w": 6, + "x": 6, + "y": 13 }, "id": 12, "legend": { @@ -753,10 +836,10 @@ "datasource": "gdev-mssql-ds-tests", "fill": 2, "gridPos": { - "h": 9, - "w": 8, - "x": 16, - "y": 16 + "h": 6, + "w": 6, + "x": 12, + "y": 13 }, "id": 13, "legend": { @@ -828,6 +911,89 @@ "alignLevel": null } }, + { + "aliasColors": {}, + "bars": true, + "dashLength": 10, + "dashes": false, + "datasource": "gdev-mssql-ds-tests", + "fill": 2, + "gridPos": { + "h": 6, + "w": 6, + "x": 18, + "y": 13 + }, + "id": 37, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": false, + "linewidth": 2, + "links": [], + "nullPointMode": "null", + "percentage": false, + "pointradius": 3, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": true, + "targets": [ + { + "alias": "", + "format": "time_series", + "rawSql": "SELECT $__timeGroupAlias(time, '$summarize', previous), sum(value) as value FROM metric WHERE $__timeFilter(time) GROUP BY $__timeGroup(time, '$summarize') ORDER BY 1", + "refId": "A" + } + ], + "thresholds": [], + "timeFrom": null, + "timeShift": null, + "title": "Metrics - timeGroup macro $summarize with fill(previous)", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, { "aliasColors": {}, "bars": false, @@ -839,7 +1005,7 @@ "h": 8, "w": 12, "x": 0, - "y": 25 + "y": 19 }, "id": 27, "legend": { @@ -926,7 +1092,7 @@ "h": 8, "w": 12, "x": 12, - "y": 25 + "y": 19 }, "id": 5, "legend": { @@ -1029,7 +1195,7 @@ "h": 8, "w": 12, "x": 0, - "y": 33 + "y": 27 }, "id": 4, "legend": { @@ -1116,7 +1282,7 @@ "h": 8, "w": 12, "x": 12, - "y": 33 + "y": 27 }, "id": 28, "legend": { @@ -1201,7 +1367,7 @@ "h": 8, "w": 12, "x": 0, - "y": 41 + "y": 35 }, "id": 19, "legend": { @@ -1288,7 +1454,7 @@ "h": 8, "w": 12, "x": 12, - "y": 41 + "y": 35 }, "id": 18, "legend": { @@ -1373,7 +1539,7 @@ "h": 8, "w": 12, "x": 0, - "y": 49 + "y": 43 }, "id": 17, "legend": { @@ -1460,7 +1626,7 @@ "h": 8, "w": 12, "x": 12, - "y": 49 + "y": 43 }, "id": 20, "legend": { @@ -1545,7 +1711,7 @@ "h": 8, "w": 12, "x": 0, - "y": 57 + "y": 51 }, "id": 29, "legend": { @@ -1632,7 +1798,7 @@ "h": 8, "w": 12, "x": 12, - "y": 57 + "y": 51 }, "id": 30, "legend": { @@ -1719,7 +1885,7 @@ "h": 8, "w": 12, "x": 0, - "y": 65 + "y": 59 }, "id": 14, "legend": { @@ -1807,7 +1973,7 @@ "h": 8, "w": 12, "x": 12, - "y": 65 + "y": 59 }, "id": 15, "legend": { @@ -1894,7 +2060,7 @@ "h": 8, "w": 12, "x": 0, - "y": 73 + "y": 67 }, "id": 25, "legend": { @@ -1982,7 +2148,7 @@ "h": 8, "w": 12, "x": 12, - "y": 73 + "y": 67 }, "id": 22, "legend": { @@ -2069,7 +2235,7 @@ "h": 8, "w": 12, "x": 0, - "y": 81 + "y": 75 }, "id": 21, "legend": { @@ -2157,7 +2323,7 @@ "h": 8, "w": 12, "x": 12, - "y": 81 + "y": 75 }, "id": 26, "legend": { @@ -2244,7 +2410,7 @@ "h": 8, "w": 12, "x": 0, - "y": 89 + "y": 83 }, "id": 23, "legend": { @@ -2332,7 +2498,7 @@ "h": 8, "w": 12, "x": 12, - "y": 89 + "y": 83 }, "id": 24, "legend": { @@ -2542,5 +2708,5 @@ "timezone": "", "title": "Datasource tests - MSSQL (unit test)", "uid": "GlAqcPgmz", - "version": 3 + "version": 10 } \ No newline at end of file diff --git a/devenv/dev-dashboards/datasource_tests_mysql_unittest.json b/devenv/dev-dashboards/datasource_tests_mysql_unittest.json index f684186084a..cec8ebe9d02 100644 --- a/devenv/dev-dashboards/datasource_tests_mysql_unittest.json +++ b/devenv/dev-dashboards/datasource_tests_mysql_unittest.json @@ -64,7 +64,7 @@ "editable": true, "gnetId": null, "graphTooltip": 0, - "iteration": 1532949531280, + "iteration": 1533714324007, "links": [], "panels": [ { @@ -338,8 +338,8 @@ "datasource": "gdev-mysql-ds-tests", "fill": 2, "gridPos": { - "h": 9, - "w": 8, + "h": 6, + "w": 6, "x": 0, "y": 7 }, @@ -421,9 +421,9 @@ "datasource": "gdev-mysql-ds-tests", "fill": 2, "gridPos": { - "h": 9, - "w": 8, - "x": 8, + "h": 6, + "w": 6, + "x": 6, "y": 7 }, "id": 9, @@ -504,9 +504,9 @@ "datasource": "gdev-mysql-ds-tests", "fill": 2, "gridPos": { - "h": 9, - "w": 8, - "x": 16, + "h": 6, + "w": 6, + "x": 12, "y": 7 }, "id": 10, @@ -579,6 +579,89 @@ "alignLevel": null } }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "gdev-mysql-ds-tests", + "fill": 2, + "gridPos": { + "h": 6, + "w": 6, + "x": 18, + "y": 7 + }, + "id": 36, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 2, + "links": [], + "nullPointMode": "null", + "percentage": false, + "pointradius": 3, + "points": true, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": true, + "targets": [ + { + "alias": "", + "format": "time_series", + "rawSql": "SELECT $__timeGroupAlias(time, '5m', previous), avg(value) as value FROM metric WHERE $__timeFilter(time) GROUP BY 1 ORDER BY 1", + "refId": "A" + } + ], + "thresholds": [], + "timeFrom": null, + "timeShift": null, + "title": "timeGroup macro 5m with fill(previous)", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": "0", + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, { "aliasColors": {}, "bars": true, @@ -587,10 +670,10 @@ "datasource": "gdev-mysql-ds-tests", "fill": 2, "gridPos": { - "h": 9, - "w": 8, + "h": 6, + "w": 6, "x": 0, - "y": 16 + "y": 13 }, "id": 16, "legend": { @@ -670,10 +753,10 @@ "datasource": "gdev-mysql-ds-tests", "fill": 2, "gridPos": { - "h": 9, - "w": 8, - "x": 8, - "y": 16 + "h": 6, + "w": 6, + "x": 6, + "y": 13 }, "id": 12, "legend": { @@ -753,10 +836,10 @@ "datasource": "gdev-mysql-ds-tests", "fill": 2, "gridPos": { - "h": 9, - "w": 8, - "x": 16, - "y": 16 + "h": 6, + "w": 6, + "x": 12, + "y": 13 }, "id": 13, "legend": { @@ -828,6 +911,89 @@ "alignLevel": null } }, + { + "aliasColors": {}, + "bars": true, + "dashLength": 10, + "dashes": false, + "datasource": "gdev-mysql-ds-tests", + "fill": 2, + "gridPos": { + "h": 6, + "w": 6, + "x": 18, + "y": 13 + }, + "id": 37, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": false, + "linewidth": 2, + "links": [], + "nullPointMode": "null", + "percentage": false, + "pointradius": 3, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": true, + "targets": [ + { + "alias": "", + "format": "time_series", + "rawSql": "SELECT $__timeGroupAlias(time, '$summarize', previous), sum(value) as value FROM metric WHERE $__timeFilter(time) GROUP BY 1 ORDER BY 1", + "refId": "A" + } + ], + "thresholds": [], + "timeFrom": null, + "timeShift": null, + "title": "Metrics - timeGroup macro $summarize with fill(previous)", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, { "aliasColors": {}, "bars": false, @@ -839,7 +1005,7 @@ "h": 8, "w": 12, "x": 0, - "y": 25 + "y": 19 }, "id": 27, "legend": { @@ -926,7 +1092,7 @@ "h": 8, "w": 12, "x": 12, - "y": 25 + "y": 19 }, "id": 5, "legend": { @@ -1023,7 +1189,7 @@ "h": 8, "w": 12, "x": 0, - "y": 33 + "y": 27 }, "id": 4, "legend": { @@ -1110,7 +1276,7 @@ "h": 8, "w": 12, "x": 12, - "y": 33 + "y": 27 }, "id": 28, "legend": { @@ -1195,7 +1361,7 @@ "h": 8, "w": 12, "x": 0, - "y": 41 + "y": 35 }, "id": 19, "legend": { @@ -1282,7 +1448,7 @@ "h": 8, "w": 12, "x": 12, - "y": 41 + "y": 35 }, "id": 18, "legend": { @@ -1367,7 +1533,7 @@ "h": 8, "w": 12, "x": 0, - "y": 49 + "y": 43 }, "id": 17, "legend": { @@ -1454,7 +1620,7 @@ "h": 8, "w": 12, "x": 12, - "y": 49 + "y": 43 }, "id": 20, "legend": { @@ -1539,7 +1705,7 @@ "h": 8, "w": 12, "x": 0, - "y": 57 + "y": 51 }, "id": 14, "legend": { @@ -1627,7 +1793,7 @@ "h": 8, "w": 12, "x": 12, - "y": 57 + "y": 51 }, "id": 15, "legend": { @@ -1714,7 +1880,7 @@ "h": 8, "w": 12, "x": 0, - "y": 65 + "y": 59 }, "id": 25, "legend": { @@ -1802,7 +1968,7 @@ "h": 8, "w": 12, "x": 12, - "y": 65 + "y": 59 }, "id": 22, "legend": { @@ -1889,7 +2055,7 @@ "h": 8, "w": 12, "x": 0, - "y": 73 + "y": 67 }, "id": 21, "legend": { @@ -1977,7 +2143,7 @@ "h": 8, "w": 12, "x": 12, - "y": 73 + "y": 67 }, "id": 26, "legend": { @@ -2064,7 +2230,7 @@ "h": 8, "w": 12, "x": 0, - "y": 81 + "y": 75 }, "id": 23, "legend": { @@ -2152,7 +2318,7 @@ "h": 8, "w": 12, "x": 12, - "y": 81 + "y": 75 }, "id": 24, "legend": { @@ -2360,5 +2526,5 @@ "timezone": "", "title": "Datasource tests - MySQL (unittest)", "uid": "Hmf8FDkmz", - "version": 1 + "version": 9 } \ No newline at end of file diff --git a/devenv/dev-dashboards/datasource_tests_postgres_unittest.json b/devenv/dev-dashboards/datasource_tests_postgres_unittest.json index 3c2b34df78c..cc93308e116 100644 --- a/devenv/dev-dashboards/datasource_tests_postgres_unittest.json +++ b/devenv/dev-dashboards/datasource_tests_postgres_unittest.json @@ -64,7 +64,7 @@ "editable": true, "gnetId": null, "graphTooltip": 0, - "iteration": 1532951521836, + "iteration": 1533714184500, "links": [], "panels": [ { @@ -338,8 +338,8 @@ "datasource": "gdev-postgres-ds-tests", "fill": 2, "gridPos": { - "h": 9, - "w": 8, + "h": 6, + "w": 6, "x": 0, "y": 7 }, @@ -421,9 +421,9 @@ "datasource": "gdev-postgres-ds-tests", "fill": 2, "gridPos": { - "h": 9, - "w": 8, - "x": 8, + "h": 6, + "w": 6, + "x": 6, "y": 7 }, "id": 9, @@ -504,9 +504,9 @@ "datasource": "gdev-postgres-ds-tests", "fill": 2, "gridPos": { - "h": 9, - "w": 8, - "x": 16, + "h": 6, + "w": 6, + "x": 12, "y": 7 }, "id": 10, @@ -579,6 +579,89 @@ "alignLevel": null } }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "gdev-postgres-ds-tests", + "fill": 2, + "gridPos": { + "h": 6, + "w": 6, + "x": 18, + "y": 7 + }, + "id": 36, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 2, + "links": [], + "nullPointMode": "null", + "percentage": false, + "pointradius": 3, + "points": true, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": true, + "targets": [ + { + "alias": "", + "format": "time_series", + "rawSql": "SELECT $__timeGroupAlias(time, '5m', previous), avg(value) as value FROM metric WHERE $__timeFilter(time) GROUP BY 1 ORDER BY 1", + "refId": "A" + } + ], + "thresholds": [], + "timeFrom": null, + "timeShift": null, + "title": "timeGroup macro 5m with fill(previous)", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": "0", + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, { "aliasColors": {}, "bars": true, @@ -587,10 +670,10 @@ "datasource": "gdev-postgres-ds-tests", "fill": 2, "gridPos": { - "h": 9, - "w": 8, + "h": 6, + "w": 6, "x": 0, - "y": 16 + "y": 13 }, "id": 16, "legend": { @@ -670,10 +753,10 @@ "datasource": "gdev-postgres-ds-tests", "fill": 2, "gridPos": { - "h": 9, - "w": 8, - "x": 8, - "y": 16 + "h": 6, + "w": 6, + "x": 6, + "y": 13 }, "id": 12, "legend": { @@ -753,10 +836,10 @@ "datasource": "gdev-postgres-ds-tests", "fill": 2, "gridPos": { - "h": 9, - "w": 8, - "x": 16, - "y": 16 + "h": 6, + "w": 6, + "x": 12, + "y": 13 }, "id": 13, "legend": { @@ -828,6 +911,89 @@ "alignLevel": null } }, + { + "aliasColors": {}, + "bars": true, + "dashLength": 10, + "dashes": false, + "datasource": "gdev-postgres-ds-tests", + "fill": 2, + "gridPos": { + "h": 6, + "w": 6, + "x": 18, + "y": 13 + }, + "id": 37, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": false, + "linewidth": 2, + "links": [], + "nullPointMode": "null", + "percentage": false, + "pointradius": 3, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": true, + "targets": [ + { + "alias": "", + "format": "time_series", + "rawSql": "SELECT $__timeGroupAlias(time, '$summarize', previous), sum(value) as value FROM metric WHERE $__timeFilter(time) GROUP BY 1 ORDER BY 1", + "refId": "A" + } + ], + "thresholds": [], + "timeFrom": null, + "timeShift": null, + "title": "Metrics - timeGroup macro $summarize with fill(previous)", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, { "aliasColors": {}, "bars": false, @@ -839,7 +1005,7 @@ "h": 8, "w": 12, "x": 0, - "y": 25 + "y": 19 }, "id": 27, "legend": { @@ -926,7 +1092,7 @@ "h": 8, "w": 12, "x": 12, - "y": 25 + "y": 19 }, "id": 5, "legend": { @@ -1011,7 +1177,7 @@ "h": 8, "w": 12, "x": 0, - "y": 33 + "y": 27 }, "id": 4, "legend": { @@ -1098,7 +1264,7 @@ "h": 8, "w": 12, "x": 12, - "y": 33 + "y": 27 }, "id": 28, "legend": { @@ -1183,7 +1349,7 @@ "h": 8, "w": 12, "x": 0, - "y": 41 + "y": 35 }, "id": 19, "legend": { @@ -1270,7 +1436,7 @@ "h": 8, "w": 12, "x": 12, - "y": 41 + "y": 35 }, "id": 18, "legend": { @@ -1355,7 +1521,7 @@ "h": 8, "w": 12, "x": 0, - "y": 49 + "y": 43 }, "id": 17, "legend": { @@ -1442,7 +1608,7 @@ "h": 8, "w": 12, "x": 12, - "y": 49 + "y": 43 }, "id": 20, "legend": { @@ -1527,7 +1693,7 @@ "h": 8, "w": 12, "x": 0, - "y": 57 + "y": 51 }, "id": 14, "legend": { @@ -1615,7 +1781,7 @@ "h": 8, "w": 12, "x": 12, - "y": 57 + "y": 51 }, "id": 15, "legend": { @@ -1702,7 +1868,7 @@ "h": 8, "w": 12, "x": 0, - "y": 65 + "y": 59 }, "id": 25, "legend": { @@ -1790,7 +1956,7 @@ "h": 8, "w": 12, "x": 12, - "y": 65 + "y": 59 }, "id": 22, "legend": { @@ -1877,7 +2043,7 @@ "h": 8, "w": 12, "x": 0, - "y": 73 + "y": 67 }, "id": 21, "legend": { @@ -1965,7 +2131,7 @@ "h": 8, "w": 12, "x": 12, - "y": 73 + "y": 67 }, "id": 26, "legend": { @@ -2052,7 +2218,7 @@ "h": 8, "w": 12, "x": 0, - "y": 81 + "y": 75 }, "id": 23, "legend": { @@ -2140,7 +2306,7 @@ "h": 8, "w": 12, "x": 12, - "y": 81 + "y": 75 }, "id": 24, "legend": { @@ -2352,6 +2518,5 @@ "timezone": "", "title": "Datasource tests - Postgres (unittest)", "uid": "vHQdlVziz", - "version": 1 -} - + "version": 9 +} \ No newline at end of file From beddfdd86b33a965ba30df121c76ce720e83a809 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Wed, 8 Aug 2018 10:26:05 +0200 Subject: [PATCH 333/786] add api route for retrieving teams of signed in user --- docs/sources/http_api/user.md | 33 +++++++++++++++++++++++++++++++++ pkg/api/api.go | 1 + pkg/api/user.go | 15 +++++++++++++++ 3 files changed, 49 insertions(+) diff --git a/docs/sources/http_api/user.md b/docs/sources/http_api/user.md index 134c1842851..b9047187b2d 100644 --- a/docs/sources/http_api/user.md +++ b/docs/sources/http_api/user.md @@ -363,6 +363,39 @@ Content-Type: application/json ] ``` +## Teams that the actual User is member of + +`GET /api/user/teams` + +Return a list of all teams that the current user is member of. + +**Example Request**: + +```http +GET /api/user/teams HTTP/1.1 +Accept: application/json +Content-Type: application/json +Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk +``` + +**Example Response**: + +```http +HTTP/1.1 200 +Content-Type: application/json + +[ + { + "id": 1, + "orgId": 1, + "name": "MyTestTeam", + "email": "", + "avatarUrl": "\/avatar\/3f49c15916554246daa714b9bd0ee398", + "memberCount": 1 + } +] +``` + ## Star a dashboard `POST /api/user/stars/dashboard/:dashboardId` diff --git a/pkg/api/api.go b/pkg/api/api.go index 84425fdae3d..906481bbb8a 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -120,6 +120,7 @@ func (hs *HTTPServer) registerRoutes() { userRoute.Put("/", bind(m.UpdateUserCommand{}), Wrap(UpdateSignedInUser)) userRoute.Post("/using/:id", Wrap(UserSetUsingOrg)) userRoute.Get("/orgs", Wrap(GetSignedInUserOrgList)) + userRoute.Get("/teams", Wrap(GetSignedInUserTeamList)) userRoute.Post("/stars/dashboard/:id", Wrap(StarDashboard)) userRoute.Delete("/stars/dashboard/:id", Wrap(UnstarDashboard)) diff --git a/pkg/api/user.go b/pkg/api/user.go index 725c623575f..4b916202e65 100644 --- a/pkg/api/user.go +++ b/pkg/api/user.go @@ -111,6 +111,21 @@ func GetSignedInUserOrgList(c *m.ReqContext) Response { return getUserOrgList(c.UserId) } +// GET /api/user/teams +func GetSignedInUserTeamList(c *m.ReqContext) Response { + query := m.GetTeamsByUserQuery{OrgId: c.OrgId, UserId: c.UserId} + + if err := bus.Dispatch(&query); err != nil { + return Error(500, "Failed to get user teams", err) + } + + for _, team := range query.Result { + team.AvatarUrl = dtos.GetGravatarUrlWithDefault(team.Email, team.Name) + } + + return JSON(200, query.Result) +} + // GET /api/user/:id/orgs func GetUserOrgList(c *m.ReqContext) Response { return getUserOrgList(c.ParamsInt64(":id")) From 817179c09733fb4d94ab44fea1d28e7152dafadc Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Wed, 8 Aug 2018 10:33:30 +0200 Subject: [PATCH 334/786] changelog: add notes about closing #12756 [skip ci] --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4fa417be5f6..4983dbafdcd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ * **Prometheus**: Heatmap - fix unhandled error when some points are missing [#12484](https://github.com/grafana/grafana/issues/12484) * **Prometheus**: Add $interval, $interval_ms, $range, and $range_ms support for dashboard and template queries [#12597](https://github.com/grafana/grafana/issues/12597) * **Variables**: Skip unneeded extra query request when de-selecting variable values used for repeated panels [#8186](https://github.com/grafana/grafana/issues/8186), thx [@mtanda](https://github.com/mtanda) +* **Postgres/MySQL/MSSQL**: Add previous fill mode to $__timeGroup macro which will fill in previously seen value when point is missing [#12756](https://github.com/grafana/grafana/issues/12756), thx [@svenklemm](https://github.com/svenklemm) * **Postgres/MySQL/MSSQL**: Use floor rounding in $__timeGroup macro function [#12460](https://github.com/grafana/grafana/issues/12460), thx [@svenklemm](https://github.com/svenklemm) * **Postgres/MySQL/MSSQL**: Use metric column as prefix when returning multiple value columns [#12727](https://github.com/grafana/grafana/issues/12727), thx [@svenklemm](https://github.com/svenklemm) * **Postgres/MySQL/MSSQL**: New $__timeGroupAlias macro. Postgres $__timeGroup no longer automatically adds time column alias [#12749](https://github.com/grafana/grafana/issues/12749), thx [@svenklemm](https://github.com/svenklemm) From ca06893e691b07f938788af65e8d8847e05be9fc Mon Sep 17 00:00:00 2001 From: Patrick O'Carroll Date: Wed, 8 Aug 2018 10:50:27 +0200 Subject: [PATCH 335/786] removed mock-teams, now gets teams from backend --- public/app/features/org/partials/profile.html | 4 +--- public/app/features/org/profile_ctrl.ts | 10 +++------- 2 files changed, 4 insertions(+), 10 deletions(-) diff --git a/public/app/features/org/partials/profile.html b/public/app/features/org/partials/profile.html index 5cbb21f488a..790872d9789 100644 --- a/public/app/features/org/partials/profile.html +++ b/public/app/features/org/partials/profile.html @@ -32,13 +32,11 @@ Name - Members - + {{team.name}} - {{team.members}} diff --git a/public/app/features/org/profile_ctrl.ts b/public/app/features/org/profile_ctrl.ts index 361dfa9e52f..6cfcdc2e64c 100644 --- a/public/app/features/org/profile_ctrl.ts +++ b/public/app/features/org/profile_ctrl.ts @@ -28,13 +28,9 @@ export class ProfileCtrl { } getUserTeams() { - this.backendSrv.get('/api/user').then(teams => { - this.user.teams = [ - { name: 'Backend', email: 'backend@grafana.com', members: 5 }, - { name: 'Frontend', email: 'frontend@grafana.com', members: 4 }, - { name: 'Ops', email: 'ops@grafana.com', members: 6 }, - ]; - this.showTeamsList = this.user.teams.length > 1; + this.backendSrv.get('/api/user/teams').then(teams => { + this.teams = teams; + this.showTeamsList = this.teams.length > 1; }); } From a94406ac53f58e4617d30f7cd18d11613ed2476c Mon Sep 17 00:00:00 2001 From: Patrick O'Carroll Date: Wed, 8 Aug 2018 11:22:47 +0200 Subject: [PATCH 336/786] added more info about the teams --- public/app/features/org/partials/profile.html | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/public/app/features/org/partials/profile.html b/public/app/features/org/partials/profile.html index 790872d9789..b204c223138 100644 --- a/public/app/features/org/partials/profile.html +++ b/public/app/features/org/partials/profile.html @@ -31,12 +31,18 @@ + + + + + +
    NameEmailMembers
    {{team.name}}{{team.email}}{{team.memberCount}}
    From 13d0fa4b9a0ce3399b6e544b358a851df93bd4f9 Mon Sep 17 00:00:00 2001 From: Sven Klemm Date: Wed, 8 Aug 2018 12:23:41 +0200 Subject: [PATCH 337/786] add previous fill mode to query builder --- public/app/plugins/datasource/postgres/sql_part.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/app/plugins/datasource/postgres/sql_part.ts b/public/app/plugins/datasource/postgres/sql_part.ts index 52ede10dad9..fb132930747 100644 --- a/public/app/plugins/datasource/postgres/sql_part.ts +++ b/public/app/plugins/datasource/postgres/sql_part.ts @@ -94,7 +94,7 @@ register({ { name: 'fill', type: 'string', - options: ['none', 'NULL', '0'], + options: ['none', 'NULL', 'previous', '0'], }, ], defaultParams: ['$__interval', 'none'], From 8dfe4a97efb0389f8c0ea77f823670a01e8361ae Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Wed, 8 Aug 2018 16:01:01 +0200 Subject: [PATCH 338/786] use uid when linking to dashboards internally in a dashboard --- public/app/features/dashlinks/module.ts | 3 +-- public/app/features/panellinks/link_srv.ts | 4 ++++ public/app/features/panellinks/module.ts | 7 ++++++- 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/public/app/features/dashlinks/module.ts b/public/app/features/dashlinks/module.ts index 380144dbcd5..6322e39f290 100644 --- a/public/app/features/dashlinks/module.ts +++ b/public/app/features/dashlinks/module.ts @@ -144,8 +144,7 @@ export class DashLinksContainerCtrl { if (dash.id !== currentDashId) { memo.push({ title: dash.title, - url: 'dashboard/' + dash.uri, - target: link.target, + url: dash.url, icon: 'fa fa-th-large', keepTime: link.keepTime, includeVars: link.includeVars, diff --git a/public/app/features/panellinks/link_srv.ts b/public/app/features/panellinks/link_srv.ts index b20294485a5..9aee17f83ed 100644 --- a/public/app/features/panellinks/link_srv.ts +++ b/public/app/features/panellinks/link_srv.ts @@ -77,6 +77,10 @@ export class LinkSrv { info.target = link.targetBlank ? '_blank' : '_self'; info.href = this.templateSrv.replace(link.url || '', scopedVars); info.title = this.templateSrv.replace(link.title || '', scopedVars); + } else if (link.url) { + info.href = link.url; + info.title = this.templateSrv.replace(link.title || '', scopedVars); + info.target = link.targetBlank ? '_blank' : ''; } else if (link.dashUri) { info.href = 'dashboard/' + link.dashUri + '?'; info.title = this.templateSrv.replace(link.title || '', scopedVars); diff --git a/public/app/features/panellinks/module.ts b/public/app/features/panellinks/module.ts index 034e99f4296..66d4bd5b37f 100644 --- a/public/app/features/panellinks/module.ts +++ b/public/app/features/panellinks/module.ts @@ -39,7 +39,12 @@ export class PanelLinksEditorCtrl { backendSrv.search({ query: link.dashboard }).then(function(hits) { var dashboard = _.find(hits, { title: link.dashboard }); if (dashboard) { - link.dashUri = dashboard.uri; + if (dashboard.url) { + link.url = dashboard.url; + } else { + // To support legacy url's + link.dashUri = dashboard.uri; + } link.title = dashboard.title; } }); From e97251fe28198055fa054e50ccd4c42d5ca6bd8e Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Wed, 8 Aug 2018 16:01:35 +0200 Subject: [PATCH 339/786] skip target _self to remove full page reload --- public/app/features/dashlinks/module.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/public/app/features/dashlinks/module.ts b/public/app/features/dashlinks/module.ts index 6322e39f290..4d80f3632e6 100644 --- a/public/app/features/dashlinks/module.ts +++ b/public/app/features/dashlinks/module.ts @@ -145,6 +145,7 @@ export class DashLinksContainerCtrl { memo.push({ title: dash.title, url: dash.url, + target: link.target === '_self' ? '' : link.target, icon: 'fa fa-th-large', keepTime: link.keepTime, includeVars: link.includeVars, From d7fb704e27daea9413b41d92b53075ec7f6b4b77 Mon Sep 17 00:00:00 2001 From: Pierre GIRAUD Date: Wed, 8 Aug 2018 15:51:13 +0200 Subject: [PATCH 340/786] Convert URL-like text to links in plugins readme --- public/app/features/plugins/plugin_edit_ctrl.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/public/app/features/plugins/plugin_edit_ctrl.ts b/public/app/features/plugins/plugin_edit_ctrl.ts index 1244e6e38f7..6aa8b2bc38f 100644 --- a/public/app/features/plugins/plugin_edit_ctrl.ts +++ b/public/app/features/plugins/plugin_edit_ctrl.ts @@ -97,7 +97,9 @@ export class PluginEditCtrl { initReadme() { return this.backendSrv.get(`/api/plugins/${this.pluginId}/markdown/readme`).then(res => { - var md = new Remarkable(); + var md = new Remarkable({ + linkify: true + }); this.readmeHtml = this.$sce.trustAsHtml(md.render(res)); }); } From c1b9bbc2cf53447e39dddf0a58ae2b5c40c87ce8 Mon Sep 17 00:00:00 2001 From: David Date: Wed, 8 Aug 2018 16:50:30 +0200 Subject: [PATCH 341/786] Explore: Query hints for prometheus (#12833) * Explore: Query hints for prometheus - time series are analyzed on response - hints are shown per query - some hints have fixes - fix rendered as link after hint - click on fix executes the fix action * Added tests for determineQueryHints() * Fix index for rate hints in explore --- public/app/containers/Explore/Explore.tsx | 107 +++++++++++++----- .../app/containers/Explore/PromQueryField.tsx | 46 ++++++-- public/app/containers/Explore/QueryRows.tsx | 25 +++- .../datasource/prometheus/datasource.ts | 103 +++++++++++++++-- .../prometheus/result_transformer.ts | 22 ++-- .../prometheus/specs/datasource.jest.ts | 51 ++++++++- .../specs/result_transformer.jest.ts | 12 +- public/sass/pages/_explore.scss | 8 ++ 8 files changed, 305 insertions(+), 69 deletions(-) diff --git a/public/app/containers/Explore/Explore.tsx b/public/app/containers/Explore/Explore.tsx index 3ee5bceae8b..dcee963e2e7 100644 --- a/public/app/containers/Explore/Explore.tsx +++ b/public/app/containers/Explore/Explore.tsx @@ -19,6 +19,16 @@ import { ensureQueries, generateQueryKey, hasQuery } from './utils/query'; const MAX_HISTORY_ITEMS = 100; +function makeHints(hints) { + const hintsByIndex = []; + hints.forEach(hint => { + if (hint) { + hintsByIndex[hint.index] = hint; + } + }); + return hintsByIndex; +} + function makeTimeSeriesList(dataList, options) { return dataList.map((seriesData, index) => { const datapoints = seriesData.datapoints || []; @@ -37,7 +47,7 @@ function makeTimeSeriesList(dataList, options) { }); } -function parseInitialState(initial: string | undefined) { +function parseUrlState(initial: string | undefined) { if (initial) { try { const parsed = JSON.parse(decodePathComponent(initial)); @@ -64,8 +74,9 @@ interface IExploreState { latency: number; loading: any; logsResult: any; - queries: any; - queryError: any; + queries: any[]; + queryErrors: any[]; + queryHints: any[]; range: any; requestOptions: any; showingGraph: boolean; @@ -82,7 +93,8 @@ export class Explore extends React.Component { constructor(props) { super(props); - const { datasource, queries, range } = parseInitialState(props.routeParams.state); + const initialState: IExploreState = props.initialState; + const { datasource, queries, range } = parseUrlState(props.routeParams.state); this.state = { datasource: null, datasourceError: null, @@ -95,7 +107,8 @@ export class Explore extends React.Component { loading: false, logsResult: null, queries: ensureQueries(queries), - queryError: null, + queryErrors: [], + queryHints: [], range: range || { ...DEFAULT_RANGE }, requestOptions: null, showingGraph: true, @@ -105,7 +118,7 @@ export class Explore extends React.Component { supportsLogs: null, supportsTable: null, tableResult: null, - ...props.initialState, + ...initialState, }; } @@ -191,6 +204,8 @@ export class Explore extends React.Component { datasourceLoading: true, graphResult: null, logsResult: null, + queryErrors: [], + queryHints: [], tableResult: null, }); const datasource = await this.props.datasourceSrv.get(option.value); @@ -199,6 +214,7 @@ export class Explore extends React.Component { onChangeQuery = (value: string, index: number, override?: boolean) => { const { queries } = this.state; + let { queryErrors, queryHints } = this.state; const prevQuery = queries[index]; const edited = override ? false : prevQuery.query !== value; const nextQuery = { @@ -208,7 +224,18 @@ export class Explore extends React.Component { }; const nextQueries = [...queries]; nextQueries[index] = nextQuery; - this.setState({ queries: nextQueries }, override ? () => this.onSubmit() : undefined); + if (override) { + queryErrors = []; + queryHints = []; + } + this.setState( + { + queryErrors, + queryHints, + queries: nextQueries, + }, + override ? () => this.onSubmit() : undefined + ); }; onChangeTime = nextRange => { @@ -255,13 +282,32 @@ export class Explore extends React.Component { }; onClickTableCell = (columnKey: string, rowValue: string) => { + this.onModifyQueries({ type: 'ADD_FILTER', key: columnKey, value: rowValue }); + }; + + onModifyQueries = (action: object, index?: number) => { const { datasource, queries } = this.state; if (datasource && datasource.modifyQuery) { - const nextQueries = queries.map(q => ({ - ...q, - edited: false, - query: datasource.modifyQuery(q.query, { addFilter: { key: columnKey, value: rowValue } }), - })); + let nextQueries; + if (index === undefined) { + // Modify all queries + nextQueries = queries.map(q => ({ + ...q, + edited: false, + query: datasource.modifyQuery(q.query, action), + })); + } else { + // Modify query only at index + nextQueries = [ + ...queries.slice(0, index), + { + ...queries[index], + edited: false, + query: datasource.modifyQuery(queries[index].query, action), + }, + ...queries.slice(index + 1), + ]; + } this.setState({ queries: nextQueries }, () => this.onSubmit()); } }; @@ -309,7 +355,7 @@ export class Explore extends React.Component { this.setState({ history }); } - buildQueryOptions(targetOptions: { format: string; instant?: boolean }) { + buildQueryOptions(targetOptions: { format: string; hinting?: boolean; instant?: boolean }) { const { datasource, queries, range } = this.state; const resolution = this.el.offsetWidth; const absoluteRange = { @@ -333,19 +379,20 @@ export class Explore extends React.Component { if (!hasQuery(queries)) { return; } - this.setState({ latency: 0, loading: true, graphResult: null, queryError: null }); + this.setState({ latency: 0, loading: true, graphResult: null, queryErrors: [], queryHints: [] }); const now = Date.now(); - const options = this.buildQueryOptions({ format: 'time_series', instant: false }); + const options = this.buildQueryOptions({ format: 'time_series', instant: false, hinting: true }); try { const res = await datasource.query(options); const result = makeTimeSeriesList(res.data, options); + const queryHints = res.hints ? makeHints(res.hints) : []; const latency = Date.now() - now; - this.setState({ latency, loading: false, graphResult: result, requestOptions: options }); + this.setState({ latency, loading: false, graphResult: result, queryHints, requestOptions: options }); this.onQuerySuccess(datasource.meta.id, queries); } catch (response) { console.error(response); const queryError = response.data ? response.data.error : response; - this.setState({ loading: false, queryError }); + this.setState({ loading: false, queryErrors: [queryError] }); } } @@ -354,7 +401,7 @@ export class Explore extends React.Component { if (!hasQuery(queries)) { return; } - this.setState({ latency: 0, loading: true, queryError: null, tableResult: null }); + this.setState({ latency: 0, loading: true, queryErrors: [], queryHints: [], tableResult: null }); const now = Date.now(); const options = this.buildQueryOptions({ format: 'table', @@ -369,7 +416,7 @@ export class Explore extends React.Component { } catch (response) { console.error(response); const queryError = response.data ? response.data.error : response; - this.setState({ loading: false, queryError }); + this.setState({ loading: false, queryErrors: [queryError] }); } } @@ -378,7 +425,7 @@ export class Explore extends React.Component { if (!hasQuery(queries)) { return; } - this.setState({ latency: 0, loading: true, queryError: null, logsResult: null }); + this.setState({ latency: 0, loading: true, queryErrors: [], queryHints: [], logsResult: null }); const now = Date.now(); const options = this.buildQueryOptions({ format: 'logs', @@ -393,7 +440,7 @@ export class Explore extends React.Component { } catch (response) { console.error(response); const queryError = response.data ? response.data.error : response; - this.setState({ loading: false, queryError }); + this.setState({ loading: false, queryErrors: [queryError] }); } } @@ -415,7 +462,8 @@ export class Explore extends React.Component { loading, logsResult, queries, - queryError, + queryErrors, + queryHints, range, requestOptions, showingGraph, @@ -449,12 +497,12 @@ export class Explore extends React.Component {
    ) : ( -
    - -
    - )} +
    + )} {!datasourceMissing ? (
    + + This option determines whether TimescaleDB features will be used. + +
    +
    +
    +
    User Permission
    From c3aad100472063957ecf869115cde521c7d5ccf9 Mon Sep 17 00:00:00 2001 From: Sven Klemm Date: Thu, 9 Aug 2018 09:19:16 +0200 Subject: [PATCH 351/786] change timescaledb to checkbox instead of select --- pkg/tsdb/postgres/macros.go | 2 +- pkg/tsdb/postgres/macros_test.go | 2 +- .../plugins/datasource/postgres/datasource.ts | 20 +------------------ .../datasource/postgres/partials/config.html | 8 +------- 4 files changed, 4 insertions(+), 28 deletions(-) diff --git a/pkg/tsdb/postgres/macros.go b/pkg/tsdb/postgres/macros.go index 4f1d3f72558..69aa04f45f5 100644 --- a/pkg/tsdb/postgres/macros.go +++ b/pkg/tsdb/postgres/macros.go @@ -131,7 +131,7 @@ func (m *postgresMacroEngine) evaluateMacro(name string, args []string) (string, } } - if m.query.DataSource.JsonData.Get("timescaledb").MustString("auto") == "enabled" { + if m.query.DataSource.JsonData.Get("timescaledb").MustBool() { return fmt.Sprintf("time_bucket('%vs',%s) AS time", interval.Seconds(), args[0]), nil } else { return fmt.Sprintf("floor(extract(epoch from %s)/%v)*%v AS time", args[0], interval.Seconds(), interval.Seconds()), nil diff --git a/pkg/tsdb/postgres/macros_test.go b/pkg/tsdb/postgres/macros_test.go index 6c4ba8305b1..8b2fd7a32f8 100644 --- a/pkg/tsdb/postgres/macros_test.go +++ b/pkg/tsdb/postgres/macros_test.go @@ -17,7 +17,7 @@ func TestMacroEngine(t *testing.T) { engine := newPostgresMacroEngine() query := &tsdb.Query{DataSource: &models.DataSource{JsonData: simplejson.New()}} queryTS := &tsdb.Query{DataSource: &models.DataSource{JsonData: simplejson.New()}} - queryTS.DataSource.JsonData.Set("timescaledb", "enabled") + queryTS.DataSource.JsonData.Set("timescaledb", true) Convey("Given a time range between 2018-04-12 00:00 and 2018-04-12 00:05", func() { from := time.Date(2018, 4, 12, 18, 0, 0, 0, time.UTC) diff --git a/public/app/plugins/datasource/postgres/datasource.ts b/public/app/plugins/datasource/postgres/datasource.ts index 88c928e425a..3d48dce45b2 100644 --- a/public/app/plugins/datasource/postgres/datasource.ts +++ b/public/app/plugins/datasource/postgres/datasource.ts @@ -123,27 +123,9 @@ export class PostgresDatasource { .then(data => this.responseParser.parseMetricFindQueryResult(refId, data)); } - testDatasource(control) { + testDatasource() { return this.metricFindQuery('SELECT 1', {}) .then(res => { - if (control.current.jsonData.timescaledb === 'auto') { - return this.metricFindQuery("SELECT 1 FROM pg_extension WHERE extname='timescaledb'", {}) - .then(res => { - if (res.length === 1) { - control.current.jsonData.timescaledb = 'enabled'; - return this.backendSrv.put('/api/datasources/' + this.id, control.current).then(settings => { - control.current = settings.datasource; - control.updateFrontendSettings(); - return { status: 'success', message: 'Database Connection OK, TimescaleDB found' }; - }); - } - throw new Error('timescaledb not found'); - }) - .catch(err => { - // query errored out or empty so timescaledb is not available - return { status: 'success', message: 'Database Connection OK' }; - }); - } return { status: 'success', message: 'Database Connection OK' }; }) .catch(err => { diff --git a/public/app/plugins/datasource/postgres/partials/config.html b/public/app/plugins/datasource/postgres/partials/config.html index 07568fdc459..14b0b03ddb5 100644 --- a/public/app/plugins/datasource/postgres/partials/config.html +++ b/public/app/plugins/datasource/postgres/partials/config.html @@ -42,13 +42,7 @@
    - -
    - - - This option determines whether TimescaleDB features will be used. - -
    +
    From acd1acba2d426270ddb54a6e9b233562ec5f1ebd Mon Sep 17 00:00:00 2001 From: Sven Klemm Date: Thu, 9 Aug 2018 09:22:02 +0200 Subject: [PATCH 352/786] revert passing ctrl to testDatasource --- public/app/features/plugins/ds_edit_ctrl.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/app/features/plugins/ds_edit_ctrl.ts b/public/app/features/plugins/ds_edit_ctrl.ts index 6e05ddc36be..542e9cc3648 100644 --- a/public/app/features/plugins/ds_edit_ctrl.ts +++ b/public/app/features/plugins/ds_edit_ctrl.ts @@ -132,7 +132,7 @@ export class DataSourceEditCtrl { this.backendSrv .withNoBackendCache(() => { return datasource - .testDatasource(this) + .testDatasource() .then(result => { this.testing.message = result.message; this.testing.status = result.status; From d2984f3b0f578423a56444516682f475842fa6e7 Mon Sep 17 00:00:00 2001 From: Sven Klemm Date: Thu, 9 Aug 2018 10:14:14 +0200 Subject: [PATCH 353/786] fix rebase error --- pkg/tsdb/postgres/macros.go | 4 ++-- pkg/tsdb/postgres/macros_test.go | 4 ++-- pkg/tsdb/postgres/postgres_test.go | 1 + 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/pkg/tsdb/postgres/macros.go b/pkg/tsdb/postgres/macros.go index 69aa04f45f5..d9f97e9262c 100644 --- a/pkg/tsdb/postgres/macros.go +++ b/pkg/tsdb/postgres/macros.go @@ -132,9 +132,9 @@ func (m *postgresMacroEngine) evaluateMacro(name string, args []string) (string, } if m.query.DataSource.JsonData.Get("timescaledb").MustBool() { - return fmt.Sprintf("time_bucket('%vs',%s) AS time", interval.Seconds(), args[0]), nil + return fmt.Sprintf("time_bucket('%vs',%s)", interval.Seconds(), args[0]), nil } else { - return fmt.Sprintf("floor(extract(epoch from %s)/%v)*%v AS time", args[0], interval.Seconds(), interval.Seconds()), nil + return fmt.Sprintf("floor(extract(epoch from %s)/%v)*%v", args[0], interval.Seconds(), interval.Seconds()), nil } case "__timeGroupAlias": tg, err := m.evaluateMacro("__timeGroup", args) diff --git a/pkg/tsdb/postgres/macros_test.go b/pkg/tsdb/postgres/macros_test.go index 8b2fd7a32f8..449331224c2 100644 --- a/pkg/tsdb/postgres/macros_test.go +++ b/pkg/tsdb/postgres/macros_test.go @@ -92,7 +92,7 @@ func TestMacroEngine(t *testing.T) { sql, err := engine.Interpolate(queryTS, timeRange, "GROUP BY $__timeGroup(time_column,'5m')") So(err, ShouldBeNil) - So(sql, ShouldEqual, "GROUP BY time_bucket('300s',time_column) AS time") + So(sql, ShouldEqual, "GROUP BY time_bucket('300s',time_column)") }) Convey("interpolate __timeGroup function with spaces between args and TimescaleDB enabled", func() { @@ -100,7 +100,7 @@ func TestMacroEngine(t *testing.T) { sql, err := engine.Interpolate(queryTS, timeRange, "GROUP BY $__timeGroup(time_column , '5m')") So(err, ShouldBeNil) - So(sql, ShouldEqual, "GROUP BY time_bucket('300s',time_column) AS time") + So(sql, ShouldEqual, "GROUP BY time_bucket('300s',time_column)") }) Convey("interpolate __timeTo function", func() { diff --git a/pkg/tsdb/postgres/postgres_test.go b/pkg/tsdb/postgres/postgres_test.go index 27888b318a9..87b7f916ca9 100644 --- a/pkg/tsdb/postgres/postgres_test.go +++ b/pkg/tsdb/postgres/postgres_test.go @@ -311,6 +311,7 @@ func TestPostgres(t *testing.T) { query := &tsdb.TsdbQuery{ Queries: []*tsdb.Query{ { + DataSource: &models.DataSource{JsonData: simplejson.New()}, Model: simplejson.NewFromAny(map[string]interface{}{ "rawSql": "SELECT $__timeGroup(time, '5m', previous), avg(value) as value FROM metric GROUP BY 1 ORDER BY 1", "format": "time_series", From 9d66eeb10caf08031d935a23ff7f15ad49a12188 Mon Sep 17 00:00:00 2001 From: David Kaltschmidt Date: Thu, 9 Aug 2018 10:21:54 +0200 Subject: [PATCH 354/786] Fix padding for metrics chooser in explore --- public/vendor/css/rc-cascader.scss | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/public/vendor/css/rc-cascader.scss b/public/vendor/css/rc-cascader.scss index 5cfaaf4961a..f6e55c62d23 100644 --- a/public/vendor/css/rc-cascader.scss +++ b/public/vendor/css/rc-cascader.scss @@ -16,7 +16,7 @@ } .rc-cascader-menus.slide-up-enter, .rc-cascader-menus.slide-up-appear { - animation-duration: .3s; + animation-duration: 0.3s; animation-fill-mode: both; transform-origin: 0 0; opacity: 0; @@ -24,7 +24,7 @@ animation-play-state: paused; } .rc-cascader-menus.slide-up-leave { - animation-duration: .3s; + animation-duration: 0.3s; animation-fill-mode: both; transform-origin: 0 0; opacity: 1; @@ -66,7 +66,7 @@ .rc-cascader-menu-item { height: 32px; line-height: 32px; - padding: 0 16px; + padding: 0 2.5em 0 16px; cursor: pointer; white-space: nowrap; overflow: hidden; From 1c63f7a61ff884db153a959b6e1666ab94366562 Mon Sep 17 00:00:00 2001 From: David Date: Thu, 9 Aug 2018 10:51:04 +0200 Subject: [PATCH 355/786] Update NOTICE.md --- NOTICE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/NOTICE.md b/NOTICE.md index ca148971b62..899b2a3c3f9 100644 --- a/NOTICE.md +++ b/NOTICE.md @@ -1,5 +1,5 @@ -Copyright 2014-2017 Grafana Labs +Copyright 2014-2018 Grafana Labs This software is based on Kibana: Copyright 2012-2013 Elasticsearch BV From 584a9cd94210266afd03fd29561ef69c32a6df43 Mon Sep 17 00:00:00 2001 From: Patrick O'Carroll Date: Thu, 9 Aug 2018 11:05:20 +0200 Subject: [PATCH 356/786] [wip]added empty list cta to team list, if statement toggles view for when the list is empty or not --- public/app/containers/Teams/TeamList.tsx | 105 +++++++++++++++-------- 1 file changed, 68 insertions(+), 37 deletions(-) diff --git a/public/app/containers/Teams/TeamList.tsx b/public/app/containers/Teams/TeamList.tsx index 31406250cb3..c8331e5c8b0 100644 --- a/public/app/containers/Teams/TeamList.tsx +++ b/public/app/containers/Teams/TeamList.tsx @@ -61,48 +61,79 @@ export class TeamList extends React.Component { ); } + renderTeamList(teams) { + return ( +
    +
    +
    + +
    + + + +
    + + + + + + + + + {teams.filteredTeams.map(team => this.renderTeamMember(team))} +
    + NameEmailMembers +
    +
    +
    + ); + } + + renderEmptyList() { + return ( +
    +
    +
    There are no Teams defiened yet
    + + New team + +
    + ProTip: Something something.{' '} + Link +
    +
    +
    + ); + } + render() { const { nav, teams } = this.props; + let view; + + if (teams.filteredTeams.length > 0) { + view = this.renderTeamList(teams); + } else { + view = this.renderEmptyList(); + } + return (
    -
    -
    -
    - -
    - - - -
    - - - - - - - - - {teams.filteredTeams.map(team => this.renderTeamMember(team))} -
    - NameEmailMembers -
    -
    -
    + {view}
    ); } From f339b3502a7e54fedc58601a61185a539d9c2b3b Mon Sep 17 00:00:00 2001 From: Patrick O'Carroll Date: Thu, 9 Aug 2018 12:56:55 +0200 Subject: [PATCH 357/786] replaced confirm delete modal with deleteButton component in teams members list --- public/app/containers/Teams/TeamMembers.tsx | 18 ++++-------------- 1 file changed, 4 insertions(+), 14 deletions(-) diff --git a/public/app/containers/Teams/TeamMembers.tsx b/public/app/containers/Teams/TeamMembers.tsx index 0d0762469a0..88933e00ab1 100644 --- a/public/app/containers/Teams/TeamMembers.tsx +++ b/public/app/containers/Teams/TeamMembers.tsx @@ -2,9 +2,9 @@ import React from 'react'; import { hot } from 'react-hot-loader'; import { observer } from 'mobx-react'; import { ITeam, ITeamMember } from 'app/stores/TeamsStore/TeamsStore'; -import appEvents from 'app/core/app_events'; import SlideDown from 'app/core/components/Animations/SlideDown'; import { UserPicker, User } from 'app/core/components/Picker/UserPicker'; +import DeleteButton from 'app/core/components/DeleteButton/DeleteButton'; interface Props { team: ITeam; @@ -31,15 +31,7 @@ export class TeamMembers extends React.Component { }; removeMember(member: ITeamMember) { - appEvents.emit('confirm-modal', { - title: 'Remove Member', - text: 'Are you sure you want to remove ' + member.login + ' from this group?', - yesText: 'Remove', - icon: 'fa-warning', - onConfirm: () => { - this.removeMemberConfirmed(member); - }, - }); + this.props.team.removeMember(member); } removeMemberConfirmed(member: ITeamMember) { @@ -54,10 +46,8 @@ export class TeamMembers extends React.Component { {member.login} {member.email} - - this.removeMember(member)} className="btn btn-danger btn-mini"> - - + + this.removeMember(member)} /> ); From 1bb3cf1c3116df8992e293a8f7a27d2c1d9d20e0 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Thu, 9 Aug 2018 15:04:56 +0200 Subject: [PATCH 358/786] keep legend scroll position when series are toggled (#12845) --- public/app/plugins/panel/graph/legend.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/public/app/plugins/panel/graph/legend.ts b/public/app/plugins/panel/graph/legend.ts index af61db396ba..f5c35ad98bf 100644 --- a/public/app/plugins/panel/graph/legend.ts +++ b/public/app/plugins/panel/graph/legend.ts @@ -70,9 +70,9 @@ module.directive('graphLegend', function(popoverSrv, $timeout) { var el = $(e.currentTarget); var index = getSeriesIndexForElement(el); var seriesInfo = seriesList[index]; - var scrollPosition = $(elem.children('tbody')).scrollTop(); + const scrollPosition = legendScrollbar.scroller.scrollTop; ctrl.toggleSeries(seriesInfo, e); - $(elem.children('tbody')).scrollTop(scrollPosition); + legendScrollbar.scroller.scrollTop = scrollPosition; } function sortLegend(e) { From a4a33d80dbe1ee0dfe4a3a53a434c90919842e76 Mon Sep 17 00:00:00 2001 From: Sven Klemm Date: Thu, 9 Aug 2018 17:30:46 +0200 Subject: [PATCH 359/786] mention time_bucket in timescaledb tooltip --- public/app/plugins/datasource/postgres/partials/config.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/app/plugins/datasource/postgres/partials/config.html b/public/app/plugins/datasource/postgres/partials/config.html index 14b0b03ddb5..a1783c09dc4 100644 --- a/public/app/plugins/datasource/postgres/partials/config.html +++ b/public/app/plugins/datasource/postgres/partials/config.html @@ -42,7 +42,7 @@
    - +
    From 1d1370d11dadc33367929a4e312242682c44cd3e Mon Sep 17 00:00:00 2001 From: Patrick O'Carroll Date: Fri, 10 Aug 2018 08:27:22 +0200 Subject: [PATCH 360/786] changed messaging --- public/app/containers/Teams/TeamList.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/app/containers/Teams/TeamList.tsx b/public/app/containers/Teams/TeamList.tsx index c8331e5c8b0..52dc28a1f95 100644 --- a/public/app/containers/Teams/TeamList.tsx +++ b/public/app/containers/Teams/TeamList.tsx @@ -107,7 +107,7 @@ export class TeamList extends React.Component { return (
    -
    There are no Teams defiened yet
    +
    You haven't created any teams yet.
    New team From 9188f7423c6340c4898792b0fba594729869d19f Mon Sep 17 00:00:00 2001 From: Tobias Skarhed Date: Mon, 6 Aug 2018 09:06:29 +0200 Subject: [PATCH 361/786] Begin conversion --- .../panel/heatmap/specs/renderer.jest.ts | 319 ++++++++++++++++++ 1 file changed, 319 insertions(+) create mode 100644 public/app/plugins/panel/heatmap/specs/renderer.jest.ts diff --git a/public/app/plugins/panel/heatmap/specs/renderer.jest.ts b/public/app/plugins/panel/heatmap/specs/renderer.jest.ts new file mode 100644 index 00000000000..4e0e8d1b6a9 --- /dev/null +++ b/public/app/plugins/panel/heatmap/specs/renderer.jest.ts @@ -0,0 +1,319 @@ +// import { describe, beforeEach, it, sinon, expect, angularMocks } from '../../../../../test/lib/common'; + +import '../module'; +import angular from 'angular'; +import $ from 'jquery'; +// import helpers from 'test/specs/helpers'; +import TimeSeries from 'app/core/time_series2'; +import moment from 'moment'; +import { Emitter } from 'app/core/core'; +import rendering from '../rendering'; +import { convertToHeatMap, convertToCards, histogramToHeatmap, calculateBucketSize } from '../heatmap_data_converter'; + +describe('grafanaHeatmap', function() { + // beforeEach(angularMocks.module('grafana.core')); + + function heatmapScenario(desc, func, elementWidth = 500) { + describe(desc, function() { + var ctx: any = {}; + + ctx.setup = function(setupFunc) { + // beforeEach( + // angularMocks.module(function($provide) { + // $provide.value('timeSrv', new helpers.TimeSrvStub()); + // }) + // ); + + beforeEach(() => { + // angularMocks.inject(function($rootScope, $compile) { + var ctrl: any = { + colorSchemes: [ + { + name: 'Oranges', + value: 'interpolateOranges', + invert: 'dark', + }, + { name: 'Reds', value: 'interpolateReds', invert: 'dark' }, + ], + // events: new Emitter(), + height: 200, + panel: { + heatmap: {}, + cards: { + cardPadding: null, + cardRound: null, + }, + color: { + mode: 'spectrum', + cardColor: '#b4ff00', + colorScale: 'linear', + exponent: 0.5, + colorScheme: 'interpolateOranges', + fillBackground: false, + }, + legend: { + show: false, + }, + xBucketSize: 1000, + xBucketNumber: null, + yBucketSize: 1, + yBucketNumber: null, + xAxis: { + show: true, + }, + yAxis: { + show: true, + format: 'short', + decimals: null, + logBase: 1, + splitFactor: null, + min: null, + max: null, + removeZeroValues: false, + }, + tooltip: { + show: true, + seriesStat: false, + showHistogram: false, + }, + highlightCards: true, + }, + renderingCompleted: jest.fn(), + hiddenSeries: {}, + dashboard: { + getTimezone: () => 'utc', + }, + range: { + from: moment.utc('01 Mar 2017 10:00:00', 'DD MMM YYYY HH:mm:ss'), + to: moment.utc('01 Mar 2017 11:00:00', 'DD MMM YYYY HH:mm:ss'), + }, + }; + + var scope = $rootScope.$new(); + scope.ctrl = ctrl; + + ctx.series = []; + ctx.series.push( + new TimeSeries({ + datapoints: [[1, 1422774000000], [2, 1422774060000]], + alias: 'series1', + }) + ); + ctx.series.push( + new TimeSeries({ + datapoints: [[2, 1422774000000], [3, 1422774060000]], + alias: 'series2', + }) + ); + + ctx.data = { + heatmapStats: { + min: 1, + max: 3, + minLog: 1, + }, + xBucketSize: ctrl.panel.xBucketSize, + yBucketSize: ctrl.panel.yBucketSize, + }; + + setupFunc(ctrl, ctx); + + let logBase = ctrl.panel.yAxis.logBase; + let bucketsData; + if (ctrl.panel.dataFormat === 'tsbuckets') { + bucketsData = histogramToHeatmap(ctx.series); + } else { + bucketsData = convertToHeatMap(ctx.series, ctx.data.yBucketSize, ctx.data.xBucketSize, logBase); + } + ctx.data.buckets = bucketsData; + + let { cards, cardStats } = convertToCards(bucketsData); + ctx.data.cards = cards; + ctx.data.cardStats = cardStats; + + let elemHtml = ` +
    +
    +
    +
    +
    `; + + var element = $.parseHTML(elemHtml); + // $compile(element)(scope); + // scope.$digest(); + + ctrl.data = ctx.data; + ctx.element = element; + rendering(scope, $(element), [], ctrl); + ctrl.events.emit('render'); + }); + }; + + func(ctx); + }); + } + + heatmapScenario('default options', function(ctx) { + ctx.setup(function(ctrl) { + ctrl.panel.yAxis.logBase = 1; + }); + + it('should draw correct Y axis', function() { + var yTicks = getTicks(ctx.element, '.axis-y'); + expect(yTicks).toEqual(['1', '2', '3']); + }); + + it('should draw correct X axis', function() { + var xTicks = getTicks(ctx.element, '.axis-x'); + let expectedTicks = [ + formatTime('01 Mar 2017 10:00:00'), + formatTime('01 Mar 2017 10:15:00'), + formatTime('01 Mar 2017 10:30:00'), + formatTime('01 Mar 2017 10:45:00'), + formatTime('01 Mar 2017 11:00:00'), + ]; + expect(xTicks).toEqual(expectedTicks); + }); + }); + + heatmapScenario('when logBase is 2', function(ctx) { + ctx.setup(function(ctrl) { + ctrl.panel.yAxis.logBase = 2; + }); + + it('should draw correct Y axis', function() { + var yTicks = getTicks(ctx.element, '.axis-y'); + expect(yTicks).toEqual(['1', '2', '4']); + }); + }); + + heatmapScenario('when logBase is 10', function(ctx) { + ctx.setup(function(ctrl, ctx) { + ctrl.panel.yAxis.logBase = 10; + + ctx.series.push( + new TimeSeries({ + datapoints: [[10, 1422774000000], [20, 1422774060000]], + alias: 'series3', + }) + ); + ctx.data.heatmapStats.max = 20; + }); + + it('should draw correct Y axis', function() { + var yTicks = getTicks(ctx.element, '.axis-y'); + expect(yTicks).toEqual(['1', '10', '100']); + }); + }); + + heatmapScenario('when logBase is 32', function(ctx) { + ctx.setup(function(ctrl) { + ctrl.panel.yAxis.logBase = 32; + + ctx.series.push( + new TimeSeries({ + datapoints: [[10, 1422774000000], [100, 1422774060000]], + alias: 'series3', + }) + ); + ctx.data.heatmapStats.max = 100; + }); + + it('should draw correct Y axis', function() { + var yTicks = getTicks(ctx.element, '.axis-y'); + expect(yTicks).toEqual(['1', '32', '1.0 K']); + }); + }); + + heatmapScenario('when logBase is 1024', function(ctx) { + ctx.setup(function(ctrl) { + ctrl.panel.yAxis.logBase = 1024; + + ctx.series.push( + new TimeSeries({ + datapoints: [[2000, 1422774000000], [300000, 1422774060000]], + alias: 'series3', + }) + ); + ctx.data.heatmapStats.max = 300000; + }); + + it('should draw correct Y axis', function() { + var yTicks = getTicks(ctx.element, '.axis-y'); + expect(yTicks).toEqual(['1', '1 K', '1.0 Mil']); + }); + }); + + heatmapScenario('when Y axis format set to "none"', function(ctx) { + ctx.setup(function(ctrl) { + ctrl.panel.yAxis.logBase = 1; + ctrl.panel.yAxis.format = 'none'; + ctx.data.heatmapStats.max = 10000; + }); + + it('should draw correct Y axis', function() { + var yTicks = getTicks(ctx.element, '.axis-y'); + expect(yTicks).toEqual(['0', '2000', '4000', '6000', '8000', '10000', '12000']); + }); + }); + + heatmapScenario('when Y axis format set to "second"', function(ctx) { + ctx.setup(function(ctrl) { + ctrl.panel.yAxis.logBase = 1; + ctrl.panel.yAxis.format = 's'; + ctx.data.heatmapStats.max = 3600; + }); + + it('should draw correct Y axis', function() { + var yTicks = getTicks(ctx.element, '.axis-y'); + expect(yTicks).toEqual(['0 ns', '17 min', '33 min', '50 min', '1.11 hour']); + }); + }); + + heatmapScenario('when data format is Time series buckets', function(ctx) { + ctx.setup(function(ctrl, ctx) { + ctrl.panel.dataFormat = 'tsbuckets'; + + const series = [ + { + alias: '1', + datapoints: [[1000, 1422774000000], [200000, 1422774060000]], + }, + { + alias: '2', + datapoints: [[3000, 1422774000000], [400000, 1422774060000]], + }, + { + alias: '3', + datapoints: [[2000, 1422774000000], [300000, 1422774060000]], + }, + ]; + ctx.series = series.map(s => new TimeSeries(s)); + + ctx.data.tsBuckets = series.map(s => s.alias).concat(''); + ctx.data.yBucketSize = 1; + let xBucketBoundSet = series[0].datapoints.map(dp => dp[1]); + ctx.data.xBucketSize = calculateBucketSize(xBucketBoundSet); + }); + + it('should draw correct Y axis', function() { + var yTicks = getTicks(ctx.element, '.axis-y'); + expect(yTicks).toEqual(['1', '2', '3', '']); + }); + }); +}); + +function getTicks(element, axisSelector) { + return element + .find(axisSelector) + .find('text') + .map(function() { + return this.textContent; + }) + .get(); +} + +function formatTime(timeStr) { + let format = 'HH:mm'; + return moment.utc(timeStr, 'DD MMM YYYY HH:mm:ss').format(format); +} From 277c73581482c49985e10cbff8fe5b9bd6670046 Mon Sep 17 00:00:00 2001 From: Patrick O'Carroll Date: Fri, 10 Aug 2018 11:31:35 +0200 Subject: [PATCH 362/786] replaced with EmptyListCta --- public/app/containers/Teams/TeamList.tsx | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/public/app/containers/Teams/TeamList.tsx b/public/app/containers/Teams/TeamList.tsx index 52dc28a1f95..06b2d20245f 100644 --- a/public/app/containers/Teams/TeamList.tsx +++ b/public/app/containers/Teams/TeamList.tsx @@ -6,6 +6,7 @@ import { NavStore } from 'app/stores/NavStore/NavStore'; import { TeamsStore, ITeam } from 'app/stores/TeamsStore/TeamsStore'; import { BackendSrv } from 'app/core/services/backend_srv'; import DeleteButton from 'app/core/components/DeleteButton/DeleteButton'; +import EmptyListCTA from 'app/core/components/EmptyListCTA/EmptyListCTA'; interface Props { nav: typeof NavStore.Type; @@ -106,16 +107,18 @@ export class TeamList extends React.Component { renderEmptyList() { return (
    -
    -
    You haven't created any teams yet.
    - - New team - -
    - ProTip: Something something.{' '} - Link -
    -
    +
    ); } From e832f91fb6331ed76ae7fa94e714544c0be516ec Mon Sep 17 00:00:00 2001 From: David Kaltschmidt Date: Fri, 10 Aug 2018 13:37:15 +0200 Subject: [PATCH 363/786] Fix initial state in split explore - remove `edited` from query state to reset queries - clear more properties in state --- public/app/containers/Explore/Explore.tsx | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/public/app/containers/Explore/Explore.tsx b/public/app/containers/Explore/Explore.tsx index 9620ac4f91b..d161e7689cf 100644 --- a/public/app/containers/Explore/Explore.tsx +++ b/public/app/containers/Explore/Explore.tsx @@ -207,6 +207,7 @@ export class Explore extends React.Component { datasourceError: null, datasourceLoading: true, graphResult: null, + latency: 0, logsResult: null, queryErrors: [], queryHints: [], @@ -254,7 +255,10 @@ export class Explore extends React.Component { this.setState({ graphResult: null, logsResult: null, + latency: 0, queries: ensureQueries(), + queryErrors: [], + queryHints: [], tableResult: null, }); }; @@ -276,8 +280,10 @@ export class Explore extends React.Component { onClickSplit = () => { const { onChangeSplit } = this.props; + const state = { ...this.state }; + state.queries = state.queries.map(({ edited, ...rest }) => rest); if (onChangeSplit) { - onChangeSplit(true, this.state); + onChangeSplit(true, state); } }; From 1f88bfd2bcb489823934267b2bcdc16681f996ee Mon Sep 17 00:00:00 2001 From: Daniel Lee Date: Fri, 10 Aug 2018 14:02:51 +0200 Subject: [PATCH 364/786] Add note for #12843 --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4983dbafdcd..198b28ca392 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -39,6 +39,7 @@ * **Graph**: Option to hide series from tooltip [#3341](https://github.com/grafana/grafana/issues/3341), thx [@mtanda](https://github.com/mtanda) * **UI**: Fix iOS home screen "app" icon and Windows 10 app experience [#12752](https://github.com/grafana/grafana/issues/12752), thx [@andig](https://github.com/andig) * **Datasource**: Fix UI issue with secret fields after updating datasource [#11270](https://github.com/grafana/grafana/issues/11270) +* **Plugins**: Convert URL-like text to links in plugins readme [#12843](https://github.com/grafana/grafana/pull/12843), thx [pgiraud](https://github.com/pgiraud) ### Breaking changes From a0fbe3c296efb2082ffb9d3fd3481d6fd1fc6a41 Mon Sep 17 00:00:00 2001 From: David Kaltschmidt Date: Fri, 10 Aug 2018 14:45:09 +0200 Subject: [PATCH 365/786] Explore: Filter out existing labels in label suggestions - a valid selector returns all possible labels from the series API - we only want to suggest the label keys that are not part of the selector yet --- .../Explore/PromQueryField.jest.tsx | 19 ++++++ .../app/containers/Explore/PromQueryField.tsx | 16 +++-- .../Explore/utils/prometheus.jest.ts | 62 ++++++++++++++----- .../containers/Explore/utils/prometheus.ts | 17 ++--- 4 files changed, 85 insertions(+), 29 deletions(-) diff --git a/public/app/containers/Explore/PromQueryField.jest.tsx b/public/app/containers/Explore/PromQueryField.jest.tsx index 350a529c89e..c82a1cd448f 100644 --- a/public/app/containers/Explore/PromQueryField.jest.tsx +++ b/public/app/containers/Explore/PromQueryField.jest.tsx @@ -94,6 +94,25 @@ describe('PromQueryField typeahead handling', () => { expect(result.suggestions).toEqual([{ items: [{ label: 'bar' }], label: 'Labels' }]); }); + it('returns label suggestions on label context but leaves out labels that already exist', () => { + const instance = shallow( + + ).instance() as PromQueryField; + const value = Plain.deserialize('{job="foo",}'); + const range = value.selection.merge({ + anchorOffset: 11, + }); + const valueWithSelection = value.change().select(range).value; + const result = instance.getTypeahead({ + text: '', + prefix: '', + wrapperClasses: ['context-labels'], + value: valueWithSelection, + }); + expect(result.context).toBe('context-labels'); + expect(result.suggestions).toEqual([{ items: [{ label: 'bar' }], label: 'Labels' }]); + }); + it('returns a refresher on label context and unavailable metric', () => { const instance = shallow( diff --git a/public/app/containers/Explore/PromQueryField.tsx b/public/app/containers/Explore/PromQueryField.tsx index 1b3ff33971d..0991f08429a 100644 --- a/public/app/containers/Explore/PromQueryField.tsx +++ b/public/app/containers/Explore/PromQueryField.tsx @@ -10,7 +10,7 @@ import PluginPrism, { setPrismTokens } from './slate-plugins/prism/index'; import PrismPromql, { FUNCTIONS } from './slate-plugins/prism/promql'; import BracesPlugin from './slate-plugins/braces'; import RunnerPlugin from './slate-plugins/runner'; -import { processLabels, RATE_RANGES, cleanText, getCleanSelector } from './utils/prometheus'; +import { processLabels, RATE_RANGES, cleanText, parseSelector } from './utils/prometheus'; import TypeaheadField, { Suggestion, @@ -328,7 +328,7 @@ class PromQueryField extends React.Component -1; + const existingKeys = parsedSelector ? parsedSelector.labelKeys : []; if ((text && text.startsWith('=')) || _.includes(wrapperClasses, 'attr-value')) { // Label values @@ -374,8 +377,11 @@ class PromQueryField extends React.Component 0) { + context = 'context-labels'; + suggestions.push({ label: `Labels`, items: possibleKeys.map(wrapLabel) }); + } } } diff --git a/public/app/containers/Explore/utils/prometheus.jest.ts b/public/app/containers/Explore/utils/prometheus.jest.ts index febaecc29b5..d12d28c6bc9 100644 --- a/public/app/containers/Explore/utils/prometheus.jest.ts +++ b/public/app/containers/Explore/utils/prometheus.jest.ts @@ -1,33 +1,61 @@ -import { getCleanSelector } from './prometheus'; +import { parseSelector } from './prometheus'; + +describe('parseSelector()', () => { + let parsed; -describe('getCleanSelector()', () => { it('returns a clean selector from an empty selector', () => { - expect(getCleanSelector('{}', 1)).toBe('{}'); + parsed = parseSelector('{}', 1); + expect(parsed.selector).toBe('{}'); + expect(parsed.labelKeys).toEqual([]); }); + it('throws if selector is broken', () => { - expect(() => getCleanSelector('{foo')).toThrow(); + expect(() => parseSelector('{foo')).toThrow(); }); + it('returns the selector sorted by label key', () => { - expect(getCleanSelector('{foo="bar"}')).toBe('{foo="bar"}'); - expect(getCleanSelector('{foo="bar",baz="xx"}')).toBe('{baz="xx",foo="bar"}'); + parsed = parseSelector('{foo="bar"}'); + expect(parsed.selector).toBe('{foo="bar"}'); + expect(parsed.labelKeys).toEqual(['foo']); + + parsed = parseSelector('{foo="bar",baz="xx"}'); + expect(parsed.selector).toBe('{baz="xx",foo="bar"}'); }); + it('returns a clean selector from an incomplete one', () => { - expect(getCleanSelector('{foo}')).toBe('{}'); - expect(getCleanSelector('{foo="bar",baz}')).toBe('{foo="bar"}'); - expect(getCleanSelector('{foo="bar",baz="}')).toBe('{foo="bar"}'); + parsed = parseSelector('{foo}'); + expect(parsed.selector).toBe('{}'); + + parsed = parseSelector('{foo="bar",baz}'); + expect(parsed.selector).toBe('{foo="bar"}'); + + parsed = parseSelector('{foo="bar",baz="}'); + expect(parsed.selector).toBe('{foo="bar"}'); }); + it('throws if not inside a selector', () => { - expect(() => getCleanSelector('foo{}', 0)).toThrow(); - expect(() => getCleanSelector('foo{} + bar{}', 5)).toThrow(); + expect(() => parseSelector('foo{}', 0)).toThrow(); + expect(() => parseSelector('foo{} + bar{}', 5)).toThrow(); }); + it('returns the selector nearest to the cursor offset', () => { - expect(() => getCleanSelector('{foo="bar"} + {foo="bar"}', 0)).toThrow(); - expect(getCleanSelector('{foo="bar"} + {foo="bar"}', 1)).toBe('{foo="bar"}'); - expect(getCleanSelector('{foo="bar"} + {baz="xx"}', 1)).toBe('{foo="bar"}'); - expect(getCleanSelector('{baz="xx"} + {foo="bar"}', 16)).toBe('{foo="bar"}'); + expect(() => parseSelector('{foo="bar"} + {foo="bar"}', 0)).toThrow(); + + parsed = parseSelector('{foo="bar"} + {foo="bar"}', 1); + expect(parsed.selector).toBe('{foo="bar"}'); + + parsed = parseSelector('{foo="bar"} + {baz="xx"}', 1); + expect(parsed.selector).toBe('{foo="bar"}'); + + parsed = parseSelector('{baz="xx"} + {foo="bar"}', 16); + expect(parsed.selector).toBe('{foo="bar"}'); }); + it('returns a selector with metric if metric is given', () => { - expect(getCleanSelector('bar{foo}', 4)).toBe('{__name__="bar"}'); - expect(getCleanSelector('baz{foo="bar"}', 12)).toBe('{__name__="baz",foo="bar"}'); + parsed = parseSelector('bar{foo}', 4); + expect(parsed.selector).toBe('{__name__="bar"}'); + + parsed = parseSelector('baz{foo="bar"}', 12); + expect(parsed.selector).toBe('{__name__="baz",foo="bar"}'); }); }); diff --git a/public/app/containers/Explore/utils/prometheus.ts b/public/app/containers/Explore/utils/prometheus.ts index ab77271076d..f5ccb848f2f 100644 --- a/public/app/containers/Explore/utils/prometheus.ts +++ b/public/app/containers/Explore/utils/prometheus.ts @@ -29,11 +29,14 @@ export const cleanText = s => s.replace(/[{}[\]="(),!~+\-*/^%]/g, '').trim(); // const cleanSelectorRegexp = /\{(\w+="[^"\n]*?")(,\w+="[^"\n]*?")*\}/; const selectorRegexp = /\{[^}]*?\}/; const labelRegexp = /\b\w+="[^"\n]*?"/g; -export function getCleanSelector(query: string, cursorOffset = 1): string { +export function parseSelector(query: string, cursorOffset = 1): { labelKeys: any[]; selector: string } { if (!query.match(selectorRegexp)) { // Special matcher for metrics if (query.match(/^\w+$/)) { - return `{__name__="${query}"}`; + return { + selector: `{__name__="${query}"}`, + labelKeys: ['__name__'], + }; } throw new Error('Query must contain a selector: ' + query); } @@ -79,10 +82,10 @@ export function getCleanSelector(query: string, cursorOffset = 1): string { } // Build sorted selector - const cleanSelector = Object.keys(labels) - .sort() - .map(key => `${key}=${labels[key]}`) - .join(','); + const labelKeys = Object.keys(labels).sort(); + const cleanSelector = labelKeys.map(key => `${key}=${labels[key]}`).join(','); - return ['{', cleanSelector, '}'].join(''); + const selectorString = ['{', cleanSelector, '}'].join(''); + + return { labelKeys, selector: selectorString }; } From 0f5945c5578b3a4e2d469a4d2fb0bf3efde2db09 Mon Sep 17 00:00:00 2001 From: David Kaltschmidt Date: Fri, 10 Aug 2018 15:29:21 +0200 Subject: [PATCH 366/786] Explore: still show rate hint if query is complex - action hint currently only works for very simple queries - show a hint w/o action otherwise --- .../datasource/prometheus/datasource.ts | 24 ++++++++++++------- .../prometheus/specs/datasource.jest.ts | 24 +++++++++++++++++++ 2 files changed, 39 insertions(+), 9 deletions(-) diff --git a/public/app/plugins/datasource/prometheus/datasource.ts b/public/app/plugins/datasource/prometheus/datasource.ts index ef440ab515d..208a7b6a2f0 100644 --- a/public/app/plugins/datasource/prometheus/datasource.ts +++ b/public/app/plugins/datasource/prometheus/datasource.ts @@ -110,10 +110,9 @@ export function determineQueryHints(series: any[], datasource?: any): any[] { // Check for monotony const datapoints: [number, number][] = s.datapoints; - const simpleMetric = query.trim().match(/^\w+$/); - if (simpleMetric && datapoints.length > 1) { + if (datapoints.length > 1) { let increasing = false; - const monotonic = datapoints.every((dp, index) => { + const monotonic = datapoints.filter(dp => dp[0] !== null).every((dp, index) => { if (index === 0) { return true; } @@ -122,18 +121,25 @@ export function determineQueryHints(series: any[], datasource?: any): any[] { return dp[0] >= datapoints[index - 1][0]; }); if (increasing && monotonic) { - const label = 'Time series is monotonously increasing.'; - return { - label, - index, - fix: { + const simpleMetric = query.trim().match(/^\w+$/); + let label = 'Time series is monotonously increasing.'; + let fix; + if (simpleMetric) { + fix = { label: 'Fix by adding rate().', action: { type: 'ADD_RATE', query, index, }, - }, + }; + } else { + label = `${label} Try applying a rate() function.`; + } + return { + label, + index, + fix, }; } } diff --git a/public/app/plugins/datasource/prometheus/specs/datasource.jest.ts b/public/app/plugins/datasource/prometheus/specs/datasource.jest.ts index a108909e6e1..fea60658332 100644 --- a/public/app/plugins/datasource/prometheus/specs/datasource.jest.ts +++ b/public/app/plugins/datasource/prometheus/specs/datasource.jest.ts @@ -213,6 +213,30 @@ describe('PrometheusDatasource', () => { }); }); + it('returns a rate hint w/o action for a complex monotonously increasing series', () => { + const series = [{ datapoints: [[23, 1000], [24, 1001]], query: 'sum(metric)', responseIndex: 0 }]; + const hints = determineQueryHints(series); + expect(hints.length).toBe(1); + expect(hints[0].label).toContain('rate()'); + expect(hints[0].fix).toBeUndefined(); + }); + + it('returns a rate hint for a monotonously increasing series with missing data', () => { + const series = [{ datapoints: [[23, 1000], [null, 1001], [24, 1002]], query: 'metric', responseIndex: 0 }]; + const hints = determineQueryHints(series); + expect(hints.length).toBe(1); + expect(hints[0]).toMatchObject({ + label: 'Time series is monotonously increasing.', + index: 0, + fix: { + action: { + type: 'ADD_RATE', + query: 'metric', + }, + }, + }); + }); + it('returns a histogram hint for a bucket series', () => { const series = [{ datapoints: [[23, 1000]], query: 'metric_bucket', responseIndex: 0 }]; const hints = determineQueryHints(series); From 076bfea3628861189a41c6e363d3311bbfe4f49b Mon Sep 17 00:00:00 2001 From: Tobias Skarhed Date: Fri, 10 Aug 2018 15:35:47 +0200 Subject: [PATCH 367/786] Rewrite heatmap to class --- .../app/plugins/panel/heatmap/heatmap_ctrl.ts | 2 +- public/app/plugins/panel/heatmap/rendering.ts | 696 +++++++++--------- 2 files changed, 353 insertions(+), 345 deletions(-) diff --git a/public/app/plugins/panel/heatmap/heatmap_ctrl.ts b/public/app/plugins/panel/heatmap/heatmap_ctrl.ts index 1d35ff2ea84..1749403edf0 100644 --- a/public/app/plugins/panel/heatmap/heatmap_ctrl.ts +++ b/public/app/plugins/panel/heatmap/heatmap_ctrl.ts @@ -358,6 +358,6 @@ export class HeatmapCtrl extends MetricsPanelCtrl { } link(scope, elem, attrs, ctrl) { - rendering(scope, elem, attrs, ctrl); + let render = new rendering(scope, elem, attrs, ctrl); } } diff --git a/public/app/plugins/panel/heatmap/rendering.ts b/public/app/plugins/panel/heatmap/rendering.ts index 54d17146532..d54eb5750cd 100644 --- a/public/app/plugins/panel/heatmap/rendering.ts +++ b/public/app/plugins/panel/heatmap/rendering.ts @@ -19,56 +19,91 @@ let MIN_CARD_SIZE = 1, Y_AXIS_TICK_PADDING = 5, MIN_SELECTION_WIDTH = 2; -export default function link(scope, elem, attrs, ctrl) { - let data, timeRange, panel, heatmap; +export default class Link { + width: number; + height: number; + yScale: any; + xScale: any; + chartWidth: number; + chartHeight: number; + chartTop: number; + chartBottom: number; + yAxisWidth: number; + xAxisHeight: number; + cardPadding: number; + cardRound: number; + cardWidth: number; + cardHeight: number; + colorScale: any; + opacityScale: any; + mouseUpHandler: any; + data: any; + panel: any; + $heatmap: any; + tooltip: HeatmapTooltip; + heatmap: any; + timeRange: any; - // $heatmap is JQuery object, but heatmap is D3 - let $heatmap = elem.find('.heatmap-panel'); - let tooltip = new HeatmapTooltip($heatmap, scope); + selection: any; + padding: any; + margin: any; + dataRangeWidingFactor: number; + constructor(private scope, private elem, attrs, private ctrl) { + // $heatmap is JQuery object, but heatmap is D3 + this.$heatmap = elem.find('.heatmap-panel'); + this.tooltip = new HeatmapTooltip(this.$heatmap, this.scope); - let width, - height, - yScale, - xScale, - chartWidth, - chartHeight, - chartTop, - chartBottom, - yAxisWidth, - xAxisHeight, - cardPadding, - cardRound, - cardWidth, - cardHeight, - colorScale, - opacityScale, - mouseUpHandler; + this.selection = { + active: false, + x1: -1, + x2: -1, + }; - let selection = { - active: false, - x1: -1, - x2: -1, - }; + this.padding = { left: 0, right: 0, top: 0, bottom: 0 }; + this.margin = { left: 25, right: 15, top: 10, bottom: 20 }; + this.dataRangeWidingFactor = DATA_RANGE_WIDING_FACTOR; - let padding = { left: 0, right: 0, top: 0, bottom: 0 }, - margin = { left: 25, right: 15, top: 10, bottom: 20 }, - dataRangeWidingFactor = DATA_RANGE_WIDING_FACTOR; + this.ctrl.events.on('render', this.onRender.bind(this)); - ctrl.events.on('render', () => { - render(); - ctrl.renderingCompleted(); - }); + this.ctrl.tickValueFormatter = this.tickValueFormatter; + ///////////////////////////// + // Selection and crosshair // + ///////////////////////////// - function setElementHeight() { + // Shared crosshair and tooltip + appEvents.on('graph-hover', this.onGraphHover.bind(this), this.scope); + + appEvents.on('graph-hover-clear', this.onGraphHoverClear.bind(this), this.scope); + + // Register selection listeners + this.$heatmap.on('mousedown', this.onMouseDown.bind(this)); + this.$heatmap.on('mousemove', this.onMouseMove.bind(this)); + this.$heatmap.on('mouseleave', this.onMouseLeave.bind(this)); + } + + onGraphHoverClear() { + this.clearCrosshair(); + } + + onGraphHover(event) { + this.drawSharedCrosshair(event.pos); + } + + onRender() { + this.render(); + this.ctrl.renderingCompleted(); + } + + setElementHeight() { try { - var height = ctrl.height || panel.height || ctrl.row.height; + var height = this.ctrl.height || this.panel.height || this.ctrl.row.height; if (_.isString(height)) { height = parseInt(height.replace('px', ''), 10); } - height -= panel.legend.show ? 28 : 11; // bottom padding and space for legend + height -= this.panel.legend.show ? 28 : 11; // bottom padding and space for legend - $heatmap.css('height', height + 'px'); + this.$heatmap.css('height', height + 'px'); return true; } catch (e) { @@ -77,7 +112,7 @@ export default function link(scope, elem, attrs, ctrl) { } } - function getYAxisWidth(elem) { + getYAxisWidth(elem) { let axis_text = elem.selectAll('.axis-y text').nodes(); let max_text_width = _.max( _.map(axis_text, text => { @@ -89,7 +124,7 @@ export default function link(scope, elem, attrs, ctrl) { return max_text_width; } - function getXAxisHeight(elem) { + getXAxisHeight(elem) { let axis_line = elem.select('.axis-x line'); if (!axis_line.empty()) { let axis_line_position = parseFloat(elem.select('.axis-x line').attr('y2')); @@ -101,16 +136,16 @@ export default function link(scope, elem, attrs, ctrl) { } } - function addXAxis() { - scope.xScale = xScale = d3 + addXAxis() { + this.scope.xScale = this.xScale = d3 .scaleTime() - .domain([timeRange.from, timeRange.to]) - .range([0, chartWidth]); + .domain([this.timeRange.from, this.timeRange.to]) + .range([0, this.chartWidth]); - let ticks = chartWidth / DEFAULT_X_TICK_SIZE_PX; - let grafanaTimeFormatter = ticksUtils.grafanaTimeFormat(ticks, timeRange.from, timeRange.to); + let ticks = this.chartWidth / DEFAULT_X_TICK_SIZE_PX; + let grafanaTimeFormatter = ticksUtils.grafanaTimeFormat(ticks, this.timeRange.from, this.timeRange.to); let timeFormat; - let dashboardTimeZone = ctrl.dashboard.getTimezone(); + let dashboardTimeZone = this.ctrl.dashboard.getTimezone(); if (dashboardTimeZone === 'utc') { timeFormat = d3.utcFormat(grafanaTimeFormatter); } else { @@ -118,100 +153,100 @@ export default function link(scope, elem, attrs, ctrl) { } let xAxis = d3 - .axisBottom(xScale) + .axisBottom(this.xScale) .ticks(ticks) .tickFormat(timeFormat) .tickPadding(X_AXIS_TICK_PADDING) - .tickSize(chartHeight); + .tickSize(this.chartHeight); - let posY = margin.top; - let posX = yAxisWidth; - heatmap + let posY = this.margin.top; + let posX = this.yAxisWidth; + this.heatmap .append('g') .attr('class', 'axis axis-x') .attr('transform', 'translate(' + posX + ',' + posY + ')') .call(xAxis); // Remove horizontal line in the top of axis labels (called domain in d3) - heatmap + this.heatmap .select('.axis-x') .select('.domain') .remove(); } - function addYAxis() { - let ticks = Math.ceil(chartHeight / DEFAULT_Y_TICK_SIZE_PX); - let tick_interval = ticksUtils.tickStep(data.heatmapStats.min, data.heatmapStats.max, ticks); - let { y_min, y_max } = wideYAxisRange(data.heatmapStats.min, data.heatmapStats.max, tick_interval); + addYAxis() { + let ticks = Math.ceil(this.chartHeight / DEFAULT_Y_TICK_SIZE_PX); + let tick_interval = ticksUtils.tickStep(this.data.heatmapStats.min, this.data.heatmapStats.max, ticks); + let { y_min, y_max } = this.wideYAxisRange(this.data.heatmapStats.min, this.data.heatmapStats.max, tick_interval); // Rewrite min and max if it have been set explicitly - y_min = panel.yAxis.min !== null ? panel.yAxis.min : y_min; - y_max = panel.yAxis.max !== null ? panel.yAxis.max : y_max; + y_min = this.panel.yAxis.min !== null ? this.panel.yAxis.min : y_min; + y_max = this.panel.yAxis.max !== null ? this.panel.yAxis.max : y_max; // Adjust ticks after Y range widening tick_interval = ticksUtils.tickStep(y_min, y_max, ticks); ticks = Math.ceil((y_max - y_min) / tick_interval); let decimalsAuto = ticksUtils.getPrecision(tick_interval); - let decimals = panel.yAxis.decimals === null ? decimalsAuto : panel.yAxis.decimals; + let decimals = this.panel.yAxis.decimals === null ? decimalsAuto : this.panel.yAxis.decimals; // Calculate scaledDecimals for log scales using tick size (as in jquery.flot.js) let flot_tick_size = ticksUtils.getFlotTickSize(y_min, y_max, ticks, decimalsAuto); let scaledDecimals = ticksUtils.getScaledDecimals(decimals, flot_tick_size); - ctrl.decimals = decimals; - ctrl.scaledDecimals = scaledDecimals; + this.ctrl.decimals = decimals; + this.ctrl.scaledDecimals = scaledDecimals; // Set default Y min and max if no data - if (_.isEmpty(data.buckets)) { + if (_.isEmpty(this.data.buckets)) { y_max = 1; y_min = -1; ticks = 3; decimals = 1; } - data.yAxis = { + this.data.yAxis = { min: y_min, max: y_max, ticks: ticks, }; - scope.yScale = yScale = d3 + this.scope.yScale = this.yScale = d3 .scaleLinear() .domain([y_min, y_max]) - .range([chartHeight, 0]); + .range([this.chartHeight, 0]); let yAxis = d3 - .axisLeft(yScale) + .axisLeft(this.yScale) .ticks(ticks) - .tickFormat(tickValueFormatter(decimals, scaledDecimals)) - .tickSizeInner(0 - width) + .tickFormat(this.tickValueFormatter(decimals, scaledDecimals)) + .tickSizeInner(0 - this.width) .tickSizeOuter(0) .tickPadding(Y_AXIS_TICK_PADDING); - heatmap + this.heatmap .append('g') .attr('class', 'axis axis-y') .call(yAxis); // Calculate Y axis width first, then move axis into visible area - let posY = margin.top; - let posX = getYAxisWidth(heatmap) + Y_AXIS_TICK_PADDING; - heatmap.select('.axis-y').attr('transform', 'translate(' + posX + ',' + posY + ')'); + let posY = this.margin.top; + let posX = this.getYAxisWidth(this.heatmap) + Y_AXIS_TICK_PADDING; + this.heatmap.select('.axis-y').attr('transform', 'translate(' + posX + ',' + posY + ')'); // Remove vertical line in the right of axis labels (called domain in d3) - heatmap + this.heatmap .select('.axis-y') .select('.domain') .remove(); } // Wide Y values range and anjust to bucket size - function wideYAxisRange(min, max, tickInterval) { - let y_widing = (max * (dataRangeWidingFactor - 1) - min * (dataRangeWidingFactor - 1)) / 2; + wideYAxisRange(min, max, tickInterval) { + let y_widing = (max * (this.dataRangeWidingFactor - 1) - min * (this.dataRangeWidingFactor - 1)) / 2; let y_min, y_max; if (tickInterval === 0) { - y_max = max * dataRangeWidingFactor; - y_min = min - min * (dataRangeWidingFactor - 1); + y_max = max * this.dataRangeWidingFactor; + y_min = min - min * (this.dataRangeWidingFactor - 1); tickInterval = (y_max - y_min) / 2; } else { y_max = Math.ceil((max + y_widing) / tickInterval) * tickInterval; @@ -226,152 +261,153 @@ export default function link(scope, elem, attrs, ctrl) { return { y_min, y_max }; } - function addLogYAxis() { - let log_base = panel.yAxis.logBase; - let { y_min, y_max } = adjustLogRange(data.heatmapStats.minLog, data.heatmapStats.max, log_base); + addLogYAxis() { + let log_base = this.panel.yAxis.logBase; + let { y_min, y_max } = this.adjustLogRange(this.data.heatmapStats.minLog, this.data.heatmapStats.max, log_base); - y_min = panel.yAxis.min && panel.yAxis.min !== '0' ? adjustLogMin(panel.yAxis.min, log_base) : y_min; - y_max = panel.yAxis.max !== null ? adjustLogMax(panel.yAxis.max, log_base) : y_max; + y_min = + this.panel.yAxis.min && this.panel.yAxis.min !== '0' ? this.adjustLogMin(this.panel.yAxis.min, log_base) : y_min; + y_max = this.panel.yAxis.max !== null ? this.adjustLogMax(this.panel.yAxis.max, log_base) : y_max; // Set default Y min and max if no data - if (_.isEmpty(data.buckets)) { + if (_.isEmpty(this.data.buckets)) { y_max = Math.pow(log_base, 2); y_min = 1; } - scope.yScale = yScale = d3 + this.scope.yScale = this.yScale = d3 .scaleLog() - .base(panel.yAxis.logBase) + .base(this.panel.yAxis.logBase) .domain([y_min, y_max]) - .range([chartHeight, 0]); + .range([this.chartHeight, 0]); - let domain = yScale.domain(); - let tick_values = logScaleTickValues(domain, log_base); + let domain = this.yScale.domain(); + let tick_values = this.logScaleTickValues(domain, log_base); let decimalsAuto = ticksUtils.getPrecision(y_min); - let decimals = panel.yAxis.decimals || decimalsAuto; + let decimals = this.panel.yAxis.decimals || decimalsAuto; // Calculate scaledDecimals for log scales using tick size (as in jquery.flot.js) let flot_tick_size = ticksUtils.getFlotTickSize(y_min, y_max, tick_values.length, decimalsAuto); let scaledDecimals = ticksUtils.getScaledDecimals(decimals, flot_tick_size); - ctrl.decimals = decimals; - ctrl.scaledDecimals = scaledDecimals; + this.ctrl.decimals = decimals; + this.ctrl.scaledDecimals = scaledDecimals; - data.yAxis = { + this.data.yAxis = { min: y_min, max: y_max, ticks: tick_values.length, }; let yAxis = d3 - .axisLeft(yScale) + .axisLeft(this.yScale) .tickValues(tick_values) - .tickFormat(tickValueFormatter(decimals, scaledDecimals)) - .tickSizeInner(0 - width) + .tickFormat(this.tickValueFormatter(decimals, scaledDecimals)) + .tickSizeInner(0 - this.width) .tickSizeOuter(0) .tickPadding(Y_AXIS_TICK_PADDING); - heatmap + this.heatmap .append('g') .attr('class', 'axis axis-y') .call(yAxis); // Calculate Y axis width first, then move axis into visible area - let posY = margin.top; - let posX = getYAxisWidth(heatmap) + Y_AXIS_TICK_PADDING; - heatmap.select('.axis-y').attr('transform', 'translate(' + posX + ',' + posY + ')'); + let posY = this.margin.top; + let posX = this.getYAxisWidth(this.heatmap) + Y_AXIS_TICK_PADDING; + this.heatmap.select('.axis-y').attr('transform', 'translate(' + posX + ',' + posY + ')'); // Set first tick as pseudo 0 if (y_min < 1) { - heatmap + this.heatmap .select('.axis-y') .select('.tick text') .text('0'); } // Remove vertical line in the right of axis labels (called domain in d3) - heatmap + this.heatmap .select('.axis-y') .select('.domain') .remove(); } - function addYAxisFromBuckets() { - const tsBuckets = data.tsBuckets; + addYAxisFromBuckets() { + const tsBuckets = this.data.tsBuckets; - scope.yScale = yScale = d3 + this.scope.yScale = this.yScale = d3 .scaleLinear() .domain([0, tsBuckets.length - 1]) - .range([chartHeight, 0]); + .range([this.chartHeight, 0]); const tick_values = _.map(tsBuckets, (b, i) => i); const decimalsAuto = _.max(_.map(tsBuckets, ticksUtils.getStringPrecision)); - const decimals = panel.yAxis.decimals === null ? decimalsAuto : panel.yAxis.decimals; - ctrl.decimals = decimals; + const decimals = this.panel.yAxis.decimals === null ? decimalsAuto : this.panel.yAxis.decimals; + this.ctrl.decimals = decimals; function tickFormatter(valIndex) { let valueFormatted = tsBuckets[valIndex]; if (!_.isNaN(_.toNumber(valueFormatted)) && valueFormatted !== '') { // Try to format numeric tick labels - valueFormatted = tickValueFormatter(decimals)(_.toNumber(valueFormatted)); + valueFormatted = this.tickValueFormatter(decimals)(_.toNumber(valueFormatted)); } return valueFormatted; } const tsBucketsFormatted = _.map(tsBuckets, (v, i) => tickFormatter(i)); - data.tsBucketsFormatted = tsBucketsFormatted; + this.data.tsBucketsFormatted = tsBucketsFormatted; let yAxis = d3 - .axisLeft(yScale) + .axisLeft(this.yScale) .tickValues(tick_values) .tickFormat(tickFormatter) - .tickSizeInner(0 - width) + .tickSizeInner(0 - this.width) .tickSizeOuter(0) .tickPadding(Y_AXIS_TICK_PADDING); - heatmap + this.heatmap .append('g') .attr('class', 'axis axis-y') .call(yAxis); // Calculate Y axis width first, then move axis into visible area - const posY = margin.top; - const posX = getYAxisWidth(heatmap) + Y_AXIS_TICK_PADDING; - heatmap.select('.axis-y').attr('transform', 'translate(' + posX + ',' + posY + ')'); + const posY = this.margin.top; + const posX = this.getYAxisWidth(this.heatmap) + Y_AXIS_TICK_PADDING; + this.heatmap.select('.axis-y').attr('transform', 'translate(' + posX + ',' + posY + ')'); // Remove vertical line in the right of axis labels (called domain in d3) - heatmap + this.heatmap .select('.axis-y') .select('.domain') .remove(); } // Adjust data range to log base - function adjustLogRange(min, max, logBase) { + adjustLogRange(min, max, logBase) { let y_min, y_max; - y_min = data.heatmapStats.minLog; - if (data.heatmapStats.minLog > 1 || !data.heatmapStats.minLog) { + y_min = this.data.heatmapStats.minLog; + if (this.data.heatmapStats.minLog > 1 || !this.data.heatmapStats.minLog) { y_min = 1; } else { - y_min = adjustLogMin(data.heatmapStats.minLog, logBase); + y_min = this.adjustLogMin(this.data.heatmapStats.minLog, logBase); } // Adjust max Y value to log base - y_max = adjustLogMax(data.heatmapStats.max, logBase); + y_max = this.adjustLogMax(this.data.heatmapStats.max, logBase); return { y_min, y_max }; } - function adjustLogMax(max, base) { + adjustLogMax(max, base) { return Math.pow(base, Math.ceil(ticksUtils.logp(max, base))); } - function adjustLogMin(min, base) { + adjustLogMin(min, base) { return Math.pow(base, Math.floor(ticksUtils.logp(min, base))); } - function logScaleTickValues(domain, base) { + logScaleTickValues(domain, base) { let domainMin = domain[0]; let domainMax = domain[1]; let tickValues = []; @@ -393,8 +429,8 @@ export default function link(scope, elem, attrs, ctrl) { return tickValues; } - function tickValueFormatter(decimals, scaledDecimals = null) { - let format = panel.yAxis.format; + tickValueFormatter(decimals, scaledDecimals = null) { + let format = this.panel.yAxis.format; return function(value) { try { return format !== 'none' ? kbn.valueFormats[format](value, decimals, scaledDecimals) : value; @@ -405,181 +441,179 @@ export default function link(scope, elem, attrs, ctrl) { }; } - ctrl.tickValueFormatter = tickValueFormatter; - - function fixYAxisTickSize() { - heatmap + fixYAxisTickSize() { + this.heatmap .select('.axis-y') .selectAll('.tick line') - .attr('x2', chartWidth); + .attr('x2', this.chartWidth); } - function addAxes() { - chartHeight = height - margin.top - margin.bottom; - chartTop = margin.top; - chartBottom = chartTop + chartHeight; + addAxes() { + this.chartHeight = this.height - this.margin.top - this.margin.bottom; + this.chartTop = this.margin.top; + this.chartBottom = this.chartTop + this.chartHeight; - if (panel.dataFormat === 'tsbuckets') { - addYAxisFromBuckets(); + if (this.panel.dataFormat === 'tsbuckets') { + this.addYAxisFromBuckets(); } else { - if (panel.yAxis.logBase === 1) { - addYAxis(); + if (this.panel.yAxis.logBase === 1) { + this.addYAxis(); } else { - addLogYAxis(); + this.addLogYAxis(); } } - yAxisWidth = getYAxisWidth(heatmap) + Y_AXIS_TICK_PADDING; - chartWidth = width - yAxisWidth - margin.right; - fixYAxisTickSize(); + this.yAxisWidth = this.getYAxisWidth(this.heatmap) + Y_AXIS_TICK_PADDING; + this.chartWidth = this.width - this.yAxisWidth - this.margin.right; + this.fixYAxisTickSize(); - addXAxis(); - xAxisHeight = getXAxisHeight(heatmap); + this.addXAxis(); + this.xAxisHeight = this.getXAxisHeight(this.heatmap); - if (!panel.yAxis.show) { - heatmap + if (!this.panel.yAxis.show) { + this.heatmap .select('.axis-y') .selectAll('line') .style('opacity', 0); } - if (!panel.xAxis.show) { - heatmap + if (!this.panel.xAxis.show) { + this.heatmap .select('.axis-x') .selectAll('line') .style('opacity', 0); } } - function addHeatmapCanvas() { - let heatmap_elem = $heatmap[0]; + addHeatmapCanvas() { + let heatmap_elem = this.$heatmap[0]; - width = Math.floor($heatmap.width()) - padding.right; - height = Math.floor($heatmap.height()) - padding.bottom; + this.width = Math.floor(this.$heatmap.width()) - this.padding.right; + this.height = Math.floor(this.$heatmap.height()) - this.padding.bottom; - cardPadding = panel.cards.cardPadding !== null ? panel.cards.cardPadding : CARD_PADDING; - cardRound = panel.cards.cardRound !== null ? panel.cards.cardRound : CARD_ROUND; + this.cardPadding = this.panel.cards.cardPadding !== null ? this.panel.cards.cardPadding : CARD_PADDING; + this.cardRound = this.panel.cards.cardRound !== null ? this.panel.cards.cardRound : CARD_ROUND; - if (heatmap) { - heatmap.remove(); + if (this.heatmap) { + this.heatmap.remove(); } - heatmap = d3 + this.heatmap = d3 .select(heatmap_elem) .append('svg') - .attr('width', width) - .attr('height', height); + .attr('width', this.width) + .attr('height', this.height); } - function addHeatmap() { - addHeatmapCanvas(); - addAxes(); + addHeatmap() { + this.addHeatmapCanvas(); + this.addAxes(); - if (panel.yAxis.logBase !== 1 && panel.dataFormat !== 'tsbuckets') { - let log_base = panel.yAxis.logBase; - let domain = yScale.domain(); - let tick_values = logScaleTickValues(domain, log_base); - data.buckets = mergeZeroBuckets(data.buckets, _.min(tick_values)); + if (this.panel.yAxis.logBase !== 1 && this.panel.dataFormat !== 'tsbuckets') { + let log_base = this.panel.yAxis.logBase; + let domain = this.yScale.domain(); + let tick_values = this.logScaleTickValues(domain, log_base); + this.data.buckets = mergeZeroBuckets(this.data.buckets, _.min(tick_values)); } - let cardsData = data.cards; - let maxValueAuto = data.cardStats.max; - let maxValue = panel.color.max || maxValueAuto; - let minValue = panel.color.min || 0; + let cardsData = this.data.cards; + let maxValueAuto = this.data.cardStats.max; + let maxValue = this.panel.color.max || maxValueAuto; + let minValue = this.panel.color.min || 0; - let colorScheme = _.find(ctrl.colorSchemes, { - value: panel.color.colorScheme, + let colorScheme = _.find(this.ctrl.colorSchemes, { + value: this.panel.color.colorScheme, }); - colorScale = getColorScale(colorScheme, contextSrv.user.lightTheme, maxValue, minValue); - opacityScale = getOpacityScale(panel.color, maxValue); - setCardSize(); + this.colorScale = getColorScale(colorScheme, contextSrv.user.lightTheme, maxValue, minValue); + this.opacityScale = getOpacityScale(this.panel.color, maxValue); + this.setCardSize(); - let cards = heatmap.selectAll('.heatmap-card').data(cardsData); + let cards = this.heatmap.selectAll('.heatmap-card').data(cardsData); cards.append('title'); cards = cards .enter() .append('rect') - .attr('x', getCardX) - .attr('width', getCardWidth) - .attr('y', getCardY) - .attr('height', getCardHeight) - .attr('rx', cardRound) - .attr('ry', cardRound) + .attr('x', this.getCardX) + .attr('width', this.getCardWidth) + .attr('y', this.getCardY) + .attr('height', this.getCardHeight) + .attr('rx', this.cardRound) + .attr('ry', this.cardRound) .attr('class', 'bordered heatmap-card') - .style('fill', getCardColor) - .style('stroke', getCardColor) + .style('fill', this.getCardColor) + .style('stroke', this.getCardColor) .style('stroke-width', 0) - .style('opacity', getCardOpacity); + .style('opacity', this.getCardOpacity); - let $cards = $heatmap.find('.heatmap-card'); + let $cards = this.$heatmap.find('.heatmap-card'); $cards .on('mouseenter', event => { - tooltip.mouseOverBucket = true; - highlightCard(event); + this.tooltip.mouseOverBucket = true; + this.highlightCard(event); }) .on('mouseleave', event => { - tooltip.mouseOverBucket = false; - resetCardHighLight(event); + this.tooltip.mouseOverBucket = false; + this.resetCardHighLight(event); }); } - function highlightCard(event) { + highlightCard(event) { let color = d3.select(event.target).style('fill'); let highlightColor = d3.color(color).darker(2); let strokeColor = d3.color(color).brighter(4); let current_card = d3.select(event.target); - tooltip.originalFillColor = color; + this.tooltip.originalFillColor = color; current_card .style('fill', highlightColor.toString()) .style('stroke', strokeColor.toString()) .style('stroke-width', 1); } - function resetCardHighLight(event) { + resetCardHighLight(event) { d3 .select(event.target) - .style('fill', tooltip.originalFillColor) - .style('stroke', tooltip.originalFillColor) + .style('fill', this.tooltip.originalFillColor) + .style('stroke', this.tooltip.originalFillColor) .style('stroke-width', 0); } - function setCardSize() { - let xGridSize = Math.floor(xScale(data.xBucketSize) - xScale(0)); - let yGridSize = Math.floor(yScale(yScale.invert(0) - data.yBucketSize)); + setCardSize() { + let xGridSize = Math.floor(this.xScale(this.data.xBucketSize) - this.xScale(0)); + let yGridSize = Math.floor(this.yScale(this.yScale.invert(0) - this.data.yBucketSize)); - if (panel.yAxis.logBase !== 1) { - let base = panel.yAxis.logBase; - let splitFactor = data.yBucketSize || 1; - yGridSize = Math.floor((yScale(1) - yScale(base)) / splitFactor); + if (this.panel.yAxis.logBase !== 1) { + let base = this.panel.yAxis.logBase; + let splitFactor = this.data.yBucketSize || 1; + yGridSize = Math.floor((this.yScale(1) - this.yScale(base)) / splitFactor); } - cardWidth = xGridSize - cardPadding * 2; - cardHeight = yGridSize ? yGridSize - cardPadding * 2 : 0; + this.cardWidth = xGridSize - this.cardPadding * 2; + this.cardHeight = yGridSize ? yGridSize - this.cardPadding * 2 : 0; } - function getCardX(d) { + getCardX(d) { let x; - if (xScale(d.x) < 0) { + if (this.xScale(d.x) < 0) { // Cut card left to prevent overlay - x = yAxisWidth + cardPadding; + x = this.yAxisWidth + this.cardPadding; } else { - x = xScale(d.x) + yAxisWidth + cardPadding; + x = this.xScale(d.x) + this.yAxisWidth + this.cardPadding; } return x; } - function getCardWidth(d) { + getCardWidth(d) { let w; - if (xScale(d.x) < 0) { + if (this.xScale(d.x) < 0) { // Cut card left to prevent overlay - let cutted_width = xScale(d.x) + cardWidth; + let cutted_width = this.xScale(d.x) + this.cardWidth; w = cutted_width > 0 ? cutted_width : 0; - } else if (xScale(d.x) + cardWidth > chartWidth) { + } else if (this.xScale(d.x) + this.cardWidth > this.chartWidth) { // Cut card right to prevent overlay - w = chartWidth - xScale(d.x) - cardPadding; + w = this.chartWidth - this.xScale(d.x) - this.cardPadding; } else { - w = cardWidth; + w = this.cardWidth; } // Card width should be MIN_CARD_SIZE at least @@ -587,138 +621,117 @@ export default function link(scope, elem, attrs, ctrl) { return w; } - function getCardY(d) { - let y = yScale(d.y) + chartTop - cardHeight - cardPadding; - if (panel.yAxis.logBase !== 1 && d.y === 0) { - y = chartBottom - cardHeight - cardPadding; + getCardY(d) { + let y = this.yScale(d.y) + this.chartTop - this.cardHeight - this.cardPadding; + if (this.panel.yAxis.logBase !== 1 && d.y === 0) { + y = this.chartBottom - this.cardHeight - this.cardPadding; } else { - if (y < chartTop) { - y = chartTop; + if (y < this.chartTop) { + y = this.chartTop; } } return y; } - function getCardHeight(d) { - let y = yScale(d.y) + chartTop - cardHeight - cardPadding; - let h = cardHeight; + getCardHeight(d) { + let y = this.yScale(d.y) + this.chartTop - this.cardHeight - this.cardPadding; + let h = this.cardHeight; - if (panel.yAxis.logBase !== 1 && d.y === 0) { - return cardHeight; + if (this.panel.yAxis.logBase !== 1 && d.y === 0) { + return this.cardHeight; } // Cut card height to prevent overlay - if (y < chartTop) { - h = yScale(d.y) - cardPadding; - } else if (yScale(d.y) > chartBottom) { - h = chartBottom - y; - } else if (y + cardHeight > chartBottom) { - h = chartBottom - y; + if (y < this.chartTop) { + h = this.yScale(d.y) - this.cardPadding; + } else if (this.yScale(d.y) > this.chartBottom) { + h = this.chartBottom - y; + } else if (y + this.cardHeight > this.chartBottom) { + h = this.chartBottom - y; } // Height can't be more than chart height - h = Math.min(h, chartHeight); + h = Math.min(h, this.chartHeight); // Card height should be MIN_CARD_SIZE at least h = Math.max(h, MIN_CARD_SIZE); return h; } - function getCardColor(d) { - if (panel.color.mode === 'opacity') { - return panel.color.cardColor; + getCardColor(d) { + if (this.panel.color.mode === 'opacity') { + return this.panel.color.cardColor; } else { - return colorScale(d.count); + return this.colorScale(d.count); } } - function getCardOpacity(d) { - if (panel.color.mode === 'opacity') { - return opacityScale(d.count); + getCardOpacity(d) { + if (this.panel.color.mode === 'opacity') { + return this.opacityScale(d.count); } else { return 1; } } - ///////////////////////////// - // Selection and crosshair // - ///////////////////////////// + onMouseDown(event) { + this.selection.active = true; + this.selection.x1 = event.offsetX; - // Shared crosshair and tooltip - appEvents.on( - 'graph-hover', - event => { - drawSharedCrosshair(event.pos); - }, - scope - ); - - appEvents.on( - 'graph-hover-clear', - () => { - clearCrosshair(); - }, - scope - ); - - function onMouseDown(event) { - selection.active = true; - selection.x1 = event.offsetX; - - mouseUpHandler = function() { - onMouseUp(); + this.mouseUpHandler = () => { + this.onMouseUp(); }; - $(document).one('mouseup', mouseUpHandler); + $(document).one('mouseup', this.mouseUpHandler); } - function onMouseUp() { - $(document).unbind('mouseup', mouseUpHandler); - mouseUpHandler = null; - selection.active = false; + onMouseUp() { + $(document).unbind('mouseup', this.mouseUpHandler); + this.mouseUpHandler = null; + this.selection.active = false; - let selectionRange = Math.abs(selection.x2 - selection.x1); - if (selection.x2 >= 0 && selectionRange > MIN_SELECTION_WIDTH) { - let timeFrom = xScale.invert(Math.min(selection.x1, selection.x2) - yAxisWidth); - let timeTo = xScale.invert(Math.max(selection.x1, selection.x2) - yAxisWidth); + let selectionRange = Math.abs(this.selection.x2 - this.selection.x1); + if (this.selection.x2 >= 0 && selectionRange > MIN_SELECTION_WIDTH) { + let timeFrom = this.xScale.invert(Math.min(this.selection.x1, this.selection.x2) - this.yAxisWidth); + let timeTo = this.xScale.invert(Math.max(this.selection.x1, this.selection.x2) - this.yAxisWidth); - ctrl.timeSrv.setTime({ + this.ctrl.timeSrv.setTime({ from: moment.utc(timeFrom), to: moment.utc(timeTo), }); } - clearSelection(); + this.clearSelection(); } - function onMouseLeave() { + onMouseLeave() { appEvents.emit('graph-hover-clear'); - clearCrosshair(); + this.clearCrosshair(); } - function onMouseMove(event) { - if (!heatmap) { + onMouseMove(event) { + if (!this.heatmap) { return; } - if (selection.active) { + if (this.selection.active) { // Clear crosshair and tooltip - clearCrosshair(); - tooltip.destroy(); + this.clearCrosshair(); + this.tooltip.destroy(); - selection.x2 = limitSelection(event.offsetX); - drawSelection(selection.x1, selection.x2); + this.selection.x2 = this.limitSelection(event.offsetX); + this.drawSelection(this.selection.x1, this.selection.x2); } else { - emitGraphHoverEvent(event); - drawCrosshair(event.offsetX); - tooltip.show(event, data); + this.emitGraphHoverEvent(event); + this.drawCrosshair(event.offsetX); + this.tooltip.show(event, this.data); } } - function emitGraphHoverEvent(event) { - let x = xScale.invert(event.offsetX - yAxisWidth).valueOf(); - let y = yScale.invert(event.offsetY); + emitGraphHoverEvent(event) { + let x = this.xScale.invert(event.offsetX - this.yAxisWidth).valueOf(); + let y = this.yScale.invert(event.offsetY); let pos = { pageX: event.pageX, pageY: event.pageY, @@ -730,105 +743,100 @@ export default function link(scope, elem, attrs, ctrl) { }; // Set minimum offset to prevent showing legend from another panel - pos.panelRelY = Math.max(event.offsetY / height, 0.001); + pos.panelRelY = Math.max(event.offsetY / this.height, 0.001); // broadcast to other graph panels that we are hovering - appEvents.emit('graph-hover', { pos: pos, panel: panel }); + appEvents.emit('graph-hover', { pos: pos, panel: this.panel }); } - function limitSelection(x2) { - x2 = Math.max(x2, yAxisWidth); - x2 = Math.min(x2, chartWidth + yAxisWidth); + limitSelection(x2) { + x2 = Math.max(x2, this.yAxisWidth); + x2 = Math.min(x2, this.chartWidth + this.yAxisWidth); return x2; } - function drawSelection(posX1, posX2) { - if (heatmap) { - heatmap.selectAll('.heatmap-selection').remove(); + drawSelection(posX1, posX2) { + if (this.heatmap) { + this.heatmap.selectAll('.heatmap-selection').remove(); let selectionX = Math.min(posX1, posX2); let selectionWidth = Math.abs(posX1 - posX2); if (selectionWidth > MIN_SELECTION_WIDTH) { - heatmap + this.heatmap .append('rect') .attr('class', 'heatmap-selection') .attr('x', selectionX) .attr('width', selectionWidth) - .attr('y', chartTop) - .attr('height', chartHeight); + .attr('y', this.chartTop) + .attr('height', this.chartHeight); } } } - function clearSelection() { - selection.x1 = -1; - selection.x2 = -1; + clearSelection() { + this.selection.x1 = -1; + this.selection.x2 = -1; - if (heatmap) { - heatmap.selectAll('.heatmap-selection').remove(); + if (this.heatmap) { + this.heatmap.selectAll('.heatmap-selection').remove(); } } - function drawCrosshair(position) { - if (heatmap) { - heatmap.selectAll('.heatmap-crosshair').remove(); + drawCrosshair(position) { + if (this.heatmap) { + this.heatmap.selectAll('.heatmap-crosshair').remove(); let posX = position; - posX = Math.max(posX, yAxisWidth); - posX = Math.min(posX, chartWidth + yAxisWidth); + posX = Math.max(posX, this.yAxisWidth); + posX = Math.min(posX, this.chartWidth + this.yAxisWidth); - heatmap + this.heatmap .append('g') .attr('class', 'heatmap-crosshair') .attr('transform', 'translate(' + posX + ',0)') .append('line') .attr('x1', 1) - .attr('y1', chartTop) + .attr('y1', this.chartTop) .attr('x2', 1) - .attr('y2', chartBottom) + .attr('y2', this.chartBottom) .attr('stroke-width', 1); } } - function drawSharedCrosshair(pos) { - if (heatmap && ctrl.dashboard.graphTooltip !== 0) { - let posX = xScale(pos.x) + yAxisWidth; - drawCrosshair(posX); + drawSharedCrosshair(pos) { + if (this.heatmap && this.ctrl.dashboard.graphTooltip !== 0) { + let posX = this.xScale(pos.x) + this.yAxisWidth; + this.drawCrosshair(posX); } } - function clearCrosshair() { - if (heatmap) { - heatmap.selectAll('.heatmap-crosshair').remove(); + clearCrosshair() { + if (this.heatmap) { + this.heatmap.selectAll('.heatmap-crosshair').remove(); } } - function render() { - data = ctrl.data; - panel = ctrl.panel; - timeRange = ctrl.range; + render() { + this.data = this.ctrl.data; + this.panel = this.ctrl.panel; + this.timeRange = this.ctrl.range; - if (!setElementHeight() || !data) { + if (!this.setElementHeight() || !this.data) { return; } // Draw default axes and return if no data - if (_.isEmpty(data.buckets)) { - addHeatmapCanvas(); - addAxes(); + if (_.isEmpty(this.data.buckets)) { + this.addHeatmapCanvas(); + this.addAxes(); return; } - addHeatmap(); - scope.yAxisWidth = yAxisWidth; - scope.xAxisHeight = xAxisHeight; - scope.chartHeight = chartHeight; - scope.chartWidth = chartWidth; - scope.chartTop = chartTop; + this.addHeatmap(); + this.scope.yAxisWidth = this.yAxisWidth; + this.scope.xAxisHeight = this.xAxisHeight; + this.scope.chartHeight = this.chartHeight; + this.scope.chartWidth = this.chartWidth; + this.scope.chartTop = this.chartTop; } - - // Register selection listeners - $heatmap.on('mousedown', onMouseDown); - $heatmap.on('mousemove', onMouseMove); - $heatmap.on('mouseleave', onMouseLeave); } From 520aad819d8b43fce404ab3452068605f044c48a Mon Sep 17 00:00:00 2001 From: Tobias Skarhed Date: Fri, 10 Aug 2018 16:30:51 +0200 Subject: [PATCH 368/786] Replace element --- .../panel/heatmap/specs/renderer.jest.ts | 23 +++++++++++-------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/public/app/plugins/panel/heatmap/specs/renderer.jest.ts b/public/app/plugins/panel/heatmap/specs/renderer.jest.ts index 4e0e8d1b6a9..7001134bd70 100644 --- a/public/app/plugins/panel/heatmap/specs/renderer.jest.ts +++ b/public/app/plugins/panel/heatmap/specs/renderer.jest.ts @@ -13,6 +13,8 @@ import { convertToHeatMap, convertToCards, histogramToHeatmap, calculateBucketSi describe('grafanaHeatmap', function() { // beforeEach(angularMocks.module('grafana.core')); + let scope = {}; + function heatmapScenario(desc, func, elementWidth = 500) { describe(desc, function() { var ctx: any = {}; @@ -89,7 +91,7 @@ describe('grafanaHeatmap', function() { }, }; - var scope = $rootScope.$new(); + // var scope = $rootScope.$new(); scope.ctrl = ctrl; ctx.series = []; @@ -131,20 +133,21 @@ describe('grafanaHeatmap', function() { ctx.data.cards = cards; ctx.data.cardStats = cardStats; - let elemHtml = ` -
    -
    -
    -
    -
    `; + // let elemHtml = ` + //
    + //
    + //
    + //
    + //
    `; - var element = $.parseHTML(elemHtml); + // var element = $.parseHTML(elemHtml); // $compile(element)(scope); // scope.$digest(); ctrl.data = ctx.data; - ctx.element = element; - rendering(scope, $(element), [], ctrl); + // ctx.element = element; + let elem = {}; + let render = new rendering(scope, elem, [], ctrl); ctrl.events.emit('render'); }); }; From 8d2aac09366ba674663761cb16af31f319ab174c Mon Sep 17 00:00:00 2001 From: Ali Anwar Date: Sat, 11 Aug 2018 23:42:31 -0700 Subject: [PATCH 369/786] Fix typo --- docs/sources/http_api/folder.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/sources/http_api/folder.md b/docs/sources/http_api/folder.md index fb318ecf58e..e8845c3b125 100644 --- a/docs/sources/http_api/folder.md +++ b/docs/sources/http_api/folder.md @@ -223,7 +223,7 @@ Status Codes: - **404** – Folder not found - **412** – Precondition failed -The **412** status code is used for explaing that you cannot update the folder and why. +The **412** status code is used for explaining that you cannot update the folder and why. There can be different reasons for this: - The folder has been changed by someone else, `status=version-mismatch` From 5fd8849d656d4ee90d24c394924010ce49f8089d Mon Sep 17 00:00:00 2001 From: Ali Anwar Date: Sat, 11 Aug 2018 23:44:15 -0700 Subject: [PATCH 370/786] Update dashboard.md --- docs/sources/http_api/dashboard.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/sources/http_api/dashboard.md b/docs/sources/http_api/dashboard.md index ea1bd7f2ef7..3df36894901 100644 --- a/docs/sources/http_api/dashboard.md +++ b/docs/sources/http_api/dashboard.md @@ -85,7 +85,7 @@ Status Codes: - **403** – Access denied - **412** – Precondition failed -The **412** status code is used for explaing that you cannot create the dashboard and why. +The **412** status code is used for explaining that you cannot create the dashboard and why. There can be different reasons for this: - The dashboard has been changed by someone else, `status=version-mismatch` From d81a23becf9b306ff7dbf473a3089e8468868135 Mon Sep 17 00:00:00 2001 From: Sven Klemm Date: Sun, 12 Aug 2018 10:51:58 +0200 Subject: [PATCH 371/786] Refactor setting fillmode This adds SetupFillmode to the tsdb package to be used by the sql datasources. --- pkg/tsdb/mssql/macros.go | 19 +++---------------- pkg/tsdb/mysql/macros.go | 18 +++--------------- pkg/tsdb/postgres/macros.go | 18 +++--------------- pkg/tsdb/sql_engine.go | 21 +++++++++++++++++++++ 4 files changed, 30 insertions(+), 46 deletions(-) diff --git a/pkg/tsdb/mssql/macros.go b/pkg/tsdb/mssql/macros.go index 42e47ce6d3c..920e3781e0c 100644 --- a/pkg/tsdb/mssql/macros.go +++ b/pkg/tsdb/mssql/macros.go @@ -6,8 +6,6 @@ import ( "strings" "time" - "strconv" - "github.com/grafana/grafana/pkg/tsdb" ) @@ -97,20 +95,9 @@ func (m *msSqlMacroEngine) evaluateMacro(name string, args []string) (string, er return "", fmt.Errorf("error parsing interval %v", args[1]) } if len(args) == 3 { - m.query.Model.Set("fill", true) - m.query.Model.Set("fillInterval", interval.Seconds()) - switch args[2] { - case "NULL": - m.query.Model.Set("fillMode", "null") - case "previous": - m.query.Model.Set("fillMode", "previous") - default: - m.query.Model.Set("fillMode", "value") - floatVal, err := strconv.ParseFloat(args[2], 64) - if err != nil { - return "", fmt.Errorf("error parsing fill value %v", args[2]) - } - m.query.Model.Set("fillValue", floatVal) + err := tsdb.SetupFillmode(m.query, interval, args[2]) + if err != nil { + return "", err } } return fmt.Sprintf("FLOOR(DATEDIFF(second, '1970-01-01', %s)/%.0f)*%.0f", args[0], interval.Seconds(), interval.Seconds()), nil diff --git a/pkg/tsdb/mysql/macros.go b/pkg/tsdb/mysql/macros.go index 905d424f29a..48fa193edd5 100644 --- a/pkg/tsdb/mysql/macros.go +++ b/pkg/tsdb/mysql/macros.go @@ -3,7 +3,6 @@ package mysql import ( "fmt" "regexp" - "strconv" "strings" "time" @@ -92,20 +91,9 @@ func (m *mySqlMacroEngine) evaluateMacro(name string, args []string) (string, er return "", fmt.Errorf("error parsing interval %v", args[1]) } if len(args) == 3 { - m.query.Model.Set("fill", true) - m.query.Model.Set("fillInterval", interval.Seconds()) - switch args[2] { - case "NULL": - m.query.Model.Set("fillMode", "null") - case "previous": - m.query.Model.Set("fillMode", "previous") - default: - m.query.Model.Set("fillMode", "value") - floatVal, err := strconv.ParseFloat(args[2], 64) - if err != nil { - return "", fmt.Errorf("error parsing fill value %v", args[2]) - } - m.query.Model.Set("fillValue", floatVal) + err := tsdb.SetupFillmode(m.query, interval, args[2]) + if err != nil { + return "", err } } return fmt.Sprintf("UNIX_TIMESTAMP(%s) DIV %.0f * %.0f", args[0], interval.Seconds(), interval.Seconds()), nil diff --git a/pkg/tsdb/postgres/macros.go b/pkg/tsdb/postgres/macros.go index aebdc55d1d7..a4b4aaa9d1e 100644 --- a/pkg/tsdb/postgres/macros.go +++ b/pkg/tsdb/postgres/macros.go @@ -3,7 +3,6 @@ package postgres import ( "fmt" "regexp" - "strconv" "strings" "time" @@ -114,20 +113,9 @@ func (m *postgresMacroEngine) evaluateMacro(name string, args []string) (string, return "", fmt.Errorf("error parsing interval %v", args[1]) } if len(args) == 3 { - m.query.Model.Set("fill", true) - m.query.Model.Set("fillInterval", interval.Seconds()) - switch args[2] { - case "NULL": - m.query.Model.Set("fillMode", "null") - case "previous": - m.query.Model.Set("fillMode", "previous") - default: - m.query.Model.Set("fillMode", "value") - floatVal, err := strconv.ParseFloat(args[2], 64) - if err != nil { - return "", fmt.Errorf("error parsing fill value %v", args[2]) - } - m.query.Model.Set("fillValue", floatVal) + err := tsdb.SetupFillmode(m.query, interval, args[2]) + if err != nil { + return "", err } } return fmt.Sprintf("floor(extract(epoch from %s)/%v)*%v", args[0], interval.Seconds(), interval.Seconds()), nil diff --git a/pkg/tsdb/sql_engine.go b/pkg/tsdb/sql_engine.go index cbf6d6b4d60..454853c7cc8 100644 --- a/pkg/tsdb/sql_engine.go +++ b/pkg/tsdb/sql_engine.go @@ -6,6 +6,7 @@ import ( "database/sql" "fmt" "math" + "strconv" "strings" "sync" "time" @@ -568,3 +569,23 @@ func ConvertSqlValueColumnToFloat(columnName string, columnValue interface{}) (n return value, nil } + +func SetupFillmode(query *Query, interval time.Duration, fillmode string) error { + query.Model.Set("fill", true) + query.Model.Set("fillInterval", interval.Seconds()) + switch fillmode { + case "NULL": + query.Model.Set("fillMode", "null") + case "previous": + query.Model.Set("fillMode", "previous") + default: + query.Model.Set("fillMode", "value") + floatVal, err := strconv.ParseFloat(fillmode, 64) + if err != nil { + return fmt.Errorf("error parsing fill value %v", fillmode) + } + query.Model.Set("fillValue", floatVal) + } + + return nil +} From 48364f0111cfdaacfd4a05eaf4da98ba94a00251 Mon Sep 17 00:00:00 2001 From: Julien Pivotto Date: Mon, 13 Aug 2018 07:53:41 +0200 Subject: [PATCH 372/786] Add support for $__range_s (#12883) Fixes #12882 Signed-off-by: Julien Pivotto --- docs/sources/features/datasources/prometheus.md | 8 ++++---- docs/sources/reference/templating.md | 2 +- public/app/plugins/datasource/prometheus/datasource.ts | 2 ++ .../datasource/prometheus/specs/datasource.jest.ts | 2 ++ 4 files changed, 9 insertions(+), 5 deletions(-) diff --git a/docs/sources/features/datasources/prometheus.md b/docs/sources/features/datasources/prometheus.md index 3a04ef92e31..611a3b4d9e2 100644 --- a/docs/sources/features/datasources/prometheus.md +++ b/docs/sources/features/datasources/prometheus.md @@ -78,9 +78,9 @@ For details of *metric names*, *label names* and *label values* are please refer #### Using interval and range variables -> Support for `$__range` and `$__range_ms` only available from Grafana v5.3 +> Support for `$__range`, `$__range_s` and `$__range_ms` only available from Grafana v5.3 -It's possible to use some global built-in variables in query variables; `$__interval`, `$__interval_ms`, `$__range` and `$__range_ms`, see [Global built-in variables](/reference/templating/#global-built-in-variables) for more information. These can be convenient to use in conjunction with the `query_result` function when you need to filter variable queries since +It's possible to use some global built-in variables in query variables; `$__interval`, `$__interval_ms`, `$__range`, `$__range_s` and `$__range_ms`, see [Global built-in variables](/reference/templating/#global-built-in-variables) for more information. These can be convenient to use in conjunction with the `query_result` function when you need to filter variable queries since `label_values` function doesn't support queries. Make sure to set the variable's `refresh` trigger to be `On Time Range Change` to get the correct instances when changing the time range on the dashboard. @@ -94,10 +94,10 @@ Query: query_result(topk(5, sum(rate(http_requests_total[$__range])) by (instanc Regex: /"([^"]+)"/ ``` -Populate a variable with the instances having a certain state over the time range shown in the dashboard: +Populate a variable with the instances having a certain state over the time range shown in the dashboard, using the more precise `$__range_s`: ``` -Query: query_result(max_over_time([$__range]) != ) +Query: query_result(max_over_time([${__range_s}s]) != ) Regex: ``` diff --git a/docs/sources/reference/templating.md b/docs/sources/reference/templating.md index ce1a1299d26..d04d56dc788 100644 --- a/docs/sources/reference/templating.md +++ b/docs/sources/reference/templating.md @@ -277,7 +277,7 @@ This variable is only available in the Singlestat panel and can be used in the p > Only available in Grafana v5.3+ -Currently only supported for Prometheus data sources. This variable represents the range for the current dashboard. It is calculated by `to - from`. It has a millisecond representation called `$__range_ms`. +Currently only supported for Prometheus data sources. This variable represents the range for the current dashboard. It is calculated by `to - from`. It has a millisecond and a second representation called `$__range_ms` and `$__range_s`. ## Repeating Panels diff --git a/public/app/plugins/datasource/prometheus/datasource.ts b/public/app/plugins/datasource/prometheus/datasource.ts index ef440ab515d..318b0f8f1fc 100644 --- a/public/app/plugins/datasource/prometheus/datasource.ts +++ b/public/app/plugins/datasource/prometheus/datasource.ts @@ -489,9 +489,11 @@ export class PrometheusDatasource { getRangeScopedVars() { let range = this.timeSrv.timeRange(); let msRange = range.to.diff(range.from); + let sRange = Math.round(msRange / 1000); let regularRange = kbn.secondsToHms(msRange / 1000); return { __range_ms: { text: msRange, value: msRange }, + __range_s: { text: sRange, value: sRange }, __range: { text: regularRange, value: regularRange }, }; } diff --git a/public/app/plugins/datasource/prometheus/specs/datasource.jest.ts b/public/app/plugins/datasource/prometheus/specs/datasource.jest.ts index a108909e6e1..4ba2e3260a7 100644 --- a/public/app/plugins/datasource/prometheus/specs/datasource.jest.ts +++ b/public/app/plugins/datasource/prometheus/specs/datasource.jest.ts @@ -321,8 +321,10 @@ describe('PrometheusDatasource', () => { it('should have the correct range and range_ms', () => { let range = ctx.templateSrvMock.replace.mock.calls[0][1].__range; let rangeMs = ctx.templateSrvMock.replace.mock.calls[0][1].__range_ms; + let rangeS = ctx.templateSrvMock.replace.mock.calls[0][1].__range_s; expect(range).toEqual({ text: '21s', value: '21s' }); expect(rangeMs).toEqual({ text: 21031, value: 21031 }); + expect(rangeS).toEqual({ text: 21, value: 21 }); }); it('should pass the default interval value', () => { From 974359534fac6e91165b05705846ab225e4867d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 13 Aug 2018 07:54:49 +0200 Subject: [PATCH 373/786] Update CHANGELOG.md --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 198b28ca392..ef0d5b98696 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,7 +14,7 @@ * **Table**: Make table sorting stable when null values exist [#12362](https://github.com/grafana/grafana/pull/12362), thx [@bz2](https://github.com/bz2) * **Prometheus**: Fix graph panel bar width issue in aligned prometheus queries [#12379](https://github.com/grafana/grafana/issues/12379) * **Prometheus**: Heatmap - fix unhandled error when some points are missing [#12484](https://github.com/grafana/grafana/issues/12484) -* **Prometheus**: Add $interval, $interval_ms, $range, and $range_ms support for dashboard and template queries [#12597](https://github.com/grafana/grafana/issues/12597) +* **Prometheus**: Add $__interval, $__interval_ms, $__range, $__range_s & $__range_ms support for dashboard and template queries [#12597](https://github.com/grafana/grafana/issues/12597) * **Variables**: Skip unneeded extra query request when de-selecting variable values used for repeated panels [#8186](https://github.com/grafana/grafana/issues/8186), thx [@mtanda](https://github.com/mtanda) * **Postgres/MySQL/MSSQL**: Add previous fill mode to $__timeGroup macro which will fill in previously seen value when point is missing [#12756](https://github.com/grafana/grafana/issues/12756), thx [@svenklemm](https://github.com/svenklemm) * **Postgres/MySQL/MSSQL**: Use floor rounding in $__timeGroup macro function [#12460](https://github.com/grafana/grafana/issues/12460), thx [@svenklemm](https://github.com/svenklemm) From 48713b76f335bc307e6648985c26717287249bed Mon Sep 17 00:00:00 2001 From: Leonard Gram Date: Thu, 9 Aug 2018 16:29:16 +0200 Subject: [PATCH 374/786] docker: makes it possible to set a specific plugin url. Originally from the grafana/grafana-docker repo, authored by @ClementGautier. --- packaging/docker/run.sh | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/packaging/docker/run.sh b/packaging/docker/run.sh index 2d2318a9210..bc001bdf90a 100755 --- a/packaging/docker/run.sh +++ b/packaging/docker/run.sh @@ -67,7 +67,13 @@ if [ ! -z "${GF_INSTALL_PLUGINS}" ]; then IFS=',' for plugin in ${GF_INSTALL_PLUGINS}; do IFS=$OLDIFS - grafana-cli --pluginsDir "${GF_PATHS_PLUGINS}" plugins install ${plugin} + if [[ $plugin =~ .*\;.* ]]; then + pluginUrl=$(echo "$plugin" | cut -d';' -f 1) + pluginWithoutUrl=$(echo "$plugin" | cut -d';' -f 2) + grafana-cli --pluginUrl "${pluginUrl}" --pluginsDir "${GF_PATHS_PLUGINS}" plugins install ${pluginWithoutUrl} + else + grafana-cli --pluginsDir "${GF_PATHS_PLUGINS}" plugins install ${plugin} + fi done fi From aeba01237d3763c3a2560304bb19365d43167901 Mon Sep 17 00:00:00 2001 From: Leonard Gram Date: Mon, 13 Aug 2018 09:20:17 +0200 Subject: [PATCH 375/786] Changelog update --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ef0d5b98696..6d1816b56b2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -40,6 +40,7 @@ * **UI**: Fix iOS home screen "app" icon and Windows 10 app experience [#12752](https://github.com/grafana/grafana/issues/12752), thx [@andig](https://github.com/andig) * **Datasource**: Fix UI issue with secret fields after updating datasource [#11270](https://github.com/grafana/grafana/issues/11270) * **Plugins**: Convert URL-like text to links in plugins readme [#12843](https://github.com/grafana/grafana/pull/12843), thx [pgiraud](https://github.com/pgiraud) +* **Docker**: Make it possible to set a specific plugin url [#12861](https://github.com/grafana/grafana/pull/12861), thx [ClementGautier](https://github.com/ClementGautier) ### Breaking changes From 1dd8192d5172fd2f321b87e8998266e3cd630980 Mon Sep 17 00:00:00 2001 From: Sven Klemm Date: Mon, 13 Aug 2018 09:57:46 +0200 Subject: [PATCH 376/786] fix datatype query --- public/app/plugins/datasource/postgres/meta_query.ts | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/public/app/plugins/datasource/postgres/meta_query.ts b/public/app/plugins/datasource/postgres/meta_query.ts index c8a65990e56..414bbe654ef 100644 --- a/public/app/plugins/datasource/postgres/meta_query.ts +++ b/public/app/plugins/datasource/postgres/meta_query.ts @@ -133,14 +133,17 @@ table_schema IN ( buildDatatypeQuery(column: string) { let query = ` -SELECT data_type +SELECT udt_name FROM information_schema.columns WHERE table_schema IN ( - SELECT CASE WHEN trim(unnest) = \'"$user"\' THEN user ELSE trim(unnest) END - FROM unnest(string_to_array(current_setting(\'search_path\'),\',\')) - LIMIT 1 + SELECT schema FROM ( + SELECT CASE WHEN trim(unnest) = \'"$user"\' THEN user ELSE trim(unnest) END as schema + FROM unnest(string_to_array(current_setting(\'search_path\'),\',\')) + ) s + WHERE EXISTS (SELECT 1 FROM information_schema.schemata WHERE schema_name = s.schema) ) +LIMIT 1 `; query += ' AND table_name = ' + this.quoteIdentAsLiteral(this.target.table); query += ' AND column_name = ' + this.quoteIdentAsLiteral(column); From a79c43420a54cad877d51d27cb5812a1fd3a3b02 Mon Sep 17 00:00:00 2001 From: Tobias Skarhed Date: Mon, 13 Aug 2018 10:57:32 +0200 Subject: [PATCH 377/786] Add mocks --- .../app/plugins/panel/heatmap/heatmap_ctrl.ts | 2 +- public/app/plugins/panel/heatmap/rendering.ts | 32 +++++++++++-------- .../panel/heatmap/specs/renderer.jest.ts | 24 ++++++++++---- 3 files changed, 36 insertions(+), 22 deletions(-) diff --git a/public/app/plugins/panel/heatmap/heatmap_ctrl.ts b/public/app/plugins/panel/heatmap/heatmap_ctrl.ts index 1749403edf0..1d35ff2ea84 100644 --- a/public/app/plugins/panel/heatmap/heatmap_ctrl.ts +++ b/public/app/plugins/panel/heatmap/heatmap_ctrl.ts @@ -358,6 +358,6 @@ export class HeatmapCtrl extends MetricsPanelCtrl { } link(scope, elem, attrs, ctrl) { - let render = new rendering(scope, elem, attrs, ctrl); + rendering(scope, elem, attrs, ctrl); } } diff --git a/public/app/plugins/panel/heatmap/rendering.ts b/public/app/plugins/panel/heatmap/rendering.ts index d54eb5750cd..5af916ac13e 100644 --- a/public/app/plugins/panel/heatmap/rendering.ts +++ b/public/app/plugins/panel/heatmap/rendering.ts @@ -19,7 +19,10 @@ let MIN_CARD_SIZE = 1, Y_AXIS_TICK_PADDING = 5, MIN_SELECTION_WIDTH = 2; -export default class Link { +export default function rendering(scope, elem, attrs, ctrl) { + return new Link(scope, elem, attrs, ctrl); +} +export class Link { width: number; height: number; yScale: any; @@ -50,7 +53,7 @@ export default class Link { dataRangeWidingFactor: number; constructor(private scope, private elem, attrs, private ctrl) { // $heatmap is JQuery object, but heatmap is D3 - this.$heatmap = elem.find('.heatmap-panel'); + this.$heatmap = this.elem.find('.heatmap-panel'); this.tooltip = new HeatmapTooltip(this.$heatmap, this.scope); this.selection = { @@ -65,7 +68,7 @@ export default class Link { this.ctrl.events.on('render', this.onRender.bind(this)); - this.ctrl.tickValueFormatter = this.tickValueFormatter; + this.ctrl.tickValueFormatter = this.tickValueFormatter.bind(this); ///////////////////////////// // Selection and crosshair // ///////////////////////////// @@ -151,7 +154,7 @@ export default class Link { } else { timeFormat = d3.timeFormat(grafanaTimeFormatter); } - + console.log(ticks); let xAxis = d3 .axisBottom(this.xScale) .ticks(ticks) @@ -345,11 +348,12 @@ export default class Link { const decimals = this.panel.yAxis.decimals === null ? decimalsAuto : this.panel.yAxis.decimals; this.ctrl.decimals = decimals; + let tickValueFormatter = this.tickValueFormatter.bind(this); function tickFormatter(valIndex) { let valueFormatted = tsBuckets[valIndex]; if (!_.isNaN(_.toNumber(valueFormatted)) && valueFormatted !== '') { // Try to format numeric tick labels - valueFormatted = this.tickValueFormatter(decimals)(_.toNumber(valueFormatted)); + valueFormatted = tickValueFormatter(decimals)(_.toNumber(valueFormatted)); } return valueFormatted; } @@ -533,17 +537,17 @@ export default class Link { cards = cards .enter() .append('rect') - .attr('x', this.getCardX) - .attr('width', this.getCardWidth) - .attr('y', this.getCardY) - .attr('height', this.getCardHeight) + .attr('x', this.getCardX.bind(this)) + .attr('width', this.getCardWidth.bind(this)) + .attr('y', this.getCardY.bind(this)) + .attr('height', this.getCardHeight.bind(this)) .attr('rx', this.cardRound) .attr('ry', this.cardRound) .attr('class', 'bordered heatmap-card') - .style('fill', this.getCardColor) - .style('stroke', this.getCardColor) + .style('fill', this.getCardColor.bind(this)) + .style('stroke', this.getCardColor.bind(this)) .style('stroke-width', 0) - .style('opacity', this.getCardOpacity); + .style('opacity', this.getCardOpacity.bind(this)); let $cards = this.$heatmap.find('.heatmap-card'); $cards @@ -683,11 +687,11 @@ export default class Link { this.onMouseUp(); }; - $(document).one('mouseup', this.mouseUpHandler); + $(document).one('mouseup', this.mouseUpHandler.bind(this)); } onMouseUp() { - $(document).unbind('mouseup', this.mouseUpHandler); + $(document).unbind('mouseup', this.mouseUpHandler.bind(this)); this.mouseUpHandler = null; this.selection.active = false; diff --git a/public/app/plugins/panel/heatmap/specs/renderer.jest.ts b/public/app/plugins/panel/heatmap/specs/renderer.jest.ts index 7001134bd70..c660761890c 100644 --- a/public/app/plugins/panel/heatmap/specs/renderer.jest.ts +++ b/public/app/plugins/panel/heatmap/specs/renderer.jest.ts @@ -1,14 +1,19 @@ // import { describe, beforeEach, it, sinon, expect, angularMocks } from '../../../../../test/lib/common'; import '../module'; -import angular from 'angular'; -import $ from 'jquery'; +// import angular from 'angular'; +// import $ from 'jquery'; // import helpers from 'test/specs/helpers'; import TimeSeries from 'app/core/time_series2'; import moment from 'moment'; -import { Emitter } from 'app/core/core'; +// import { Emitter } from 'app/core/core'; import rendering from '../rendering'; import { convertToHeatMap, convertToCards, histogramToHeatmap, calculateBucketSize } from '../heatmap_data_converter'; +jest.mock('app/core/core', () => ({ + appEvents: { + on: () => {}, + }, +})); describe('grafanaHeatmap', function() { // beforeEach(angularMocks.module('grafana.core')); @@ -37,7 +42,10 @@ describe('grafanaHeatmap', function() { }, { name: 'Reds', value: 'interpolateReds', invert: 'dark' }, ], - // events: new Emitter(), + events: { + on: () => {}, + emit: () => {}, + }, height: 200, panel: { heatmap: {}, @@ -145,9 +153,11 @@ describe('grafanaHeatmap', function() { // scope.$digest(); ctrl.data = ctx.data; - // ctx.element = element; - let elem = {}; - let render = new rendering(scope, elem, [], ctrl); + ctx.element = { + find: () => ({ on: () => {} }), + on: () => {}, + }; + rendering(scope, ctx.element, [], ctrl); ctrl.events.emit('render'); }); }; From d7a0f5ee074caaed6eb8884c537d4681230518cf Mon Sep 17 00:00:00 2001 From: Leonard Gram Date: Mon, 13 Aug 2018 11:14:24 +0200 Subject: [PATCH 378/786] Removes link to deprecated docker image build --- packaging/docker/README.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packaging/docker/README.md b/packaging/docker/README.md index d80cd87aebc..cfb3c7248ef 100644 --- a/packaging/docker/README.md +++ b/packaging/docker/README.md @@ -1,7 +1,5 @@ # Grafana Docker image -[![CircleCI](https://circleci.com/gh/grafana/grafana-docker.svg?style=svg)](https://circleci.com/gh/grafana/grafana-docker) - ## Running your Grafana container Start your container binding the external port `3000`. @@ -42,4 +40,4 @@ Further documentation can be found at http://docs.grafana.org/installation/docke * Plugins dir (`/var/lib/grafana/plugins`) is no longer a separate volume ### v3.1.1 -* Make it possible to install specific plugin version https://github.com/grafana/grafana-docker/issues/59#issuecomment-260584026 \ No newline at end of file +* Make it possible to install specific plugin version https://github.com/grafana/grafana-docker/issues/59#issuecomment-260584026 From edb34a36a0cb84c0b6ec02bde71b68fddea0d6ca Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Mon, 13 Aug 2018 11:16:49 +0200 Subject: [PATCH 379/786] changelog: add notes about closing #12882 [skip ci] --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6d1816b56b2..7d5ed3378de 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,7 +14,7 @@ * **Table**: Make table sorting stable when null values exist [#12362](https://github.com/grafana/grafana/pull/12362), thx [@bz2](https://github.com/bz2) * **Prometheus**: Fix graph panel bar width issue in aligned prometheus queries [#12379](https://github.com/grafana/grafana/issues/12379) * **Prometheus**: Heatmap - fix unhandled error when some points are missing [#12484](https://github.com/grafana/grafana/issues/12484) -* **Prometheus**: Add $__interval, $__interval_ms, $__range, $__range_s & $__range_ms support for dashboard and template queries [#12597](https://github.com/grafana/grafana/issues/12597) +* **Prometheus**: Add $__interval, $__interval_ms, $__range, $__range_s & $__range_ms support for dashboard and template queries [#12597](https://github.com/grafana/grafana/issues/12597) [#12882](https://github.com/grafana/grafana/issues/12882), thx [@roidelapluie](https://github.com/roidelapluie) * **Variables**: Skip unneeded extra query request when de-selecting variable values used for repeated panels [#8186](https://github.com/grafana/grafana/issues/8186), thx [@mtanda](https://github.com/mtanda) * **Postgres/MySQL/MSSQL**: Add previous fill mode to $__timeGroup macro which will fill in previously seen value when point is missing [#12756](https://github.com/grafana/grafana/issues/12756), thx [@svenklemm](https://github.com/svenklemm) * **Postgres/MySQL/MSSQL**: Use floor rounding in $__timeGroup macro function [#12460](https://github.com/grafana/grafana/issues/12460), thx [@svenklemm](https://github.com/svenklemm) From c9bbdf244c1db8499d9723bc9a6c5e9e319b614d Mon Sep 17 00:00:00 2001 From: Sven Klemm Date: Mon, 13 Aug 2018 11:23:49 +0200 Subject: [PATCH 380/786] get timecolumn datatype on timecolumn change --- public/app/plugins/datasource/postgres/query_ctrl.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/public/app/plugins/datasource/postgres/query_ctrl.ts b/public/app/plugins/datasource/postgres/query_ctrl.ts index a196859b802..578c8875b1d 100644 --- a/public/app/plugins/datasource/postgres/query_ctrl.ts +++ b/public/app/plugins/datasource/postgres/query_ctrl.ts @@ -190,6 +190,11 @@ export class PostgresQueryCtrl extends QueryCtrl { timeColumnChanged() { this.target.timeColumn = this.timeColumnSegment.value; + this.datasource.metricFindQuery(this.metaBuilder.buildDatatypeQuery(this.target.timeColumn)).then(result => { + if (result.length === 1) { + this.target.timeColumnType = result[0]; + } + }); this.panelCtrl.refresh(); } From bdd9af0864adc5dd169c9349eae25e574f8c2937 Mon Sep 17 00:00:00 2001 From: Patrick O'Carroll Date: Mon, 13 Aug 2018 11:34:16 +0200 Subject: [PATCH 381/786] changed const members to filteredMembers to trigger get filtered members, changed input value to team.search (#12885) --- public/app/containers/Teams/TeamMembers.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/public/app/containers/Teams/TeamMembers.tsx b/public/app/containers/Teams/TeamMembers.tsx index 88933e00ab1..a6b0b04f19d 100644 --- a/public/app/containers/Teams/TeamMembers.tsx +++ b/public/app/containers/Teams/TeamMembers.tsx @@ -69,8 +69,9 @@ export class TeamMembers extends React.Component { render() { const { newTeamMember, isAdding } = this.state; - const members = this.props.team.members.values(); + const members = this.props.team.filteredMembers; const newTeamMemberValue = newTeamMember && newTeamMember.id.toString(); + const { team } = this.props; return (
    @@ -81,7 +82,7 @@ export class TeamMembers extends React.Component { type="text" className="gf-form-input" placeholder="Search members" - value={''} + value={team.search} onChange={this.onSearchQueryChange} /> From bfe28ee061ea42b27057c582f0b436cf12c46e88 Mon Sep 17 00:00:00 2001 From: Sven Klemm Date: Mon, 13 Aug 2018 12:08:14 +0200 Subject: [PATCH 382/786] Add $__unixEpochGroup macro to postgres datasource --- docs/sources/features/datasources/postgres.md | 2 ++ pkg/tsdb/postgres/macros.go | 21 +++++++++++++++++++ pkg/tsdb/postgres/macros_test.go | 12 +++++++++++ .../postgres/partials/query.editor.html | 2 ++ 4 files changed, 37 insertions(+) diff --git a/docs/sources/features/datasources/postgres.md b/docs/sources/features/datasources/postgres.md index 2be2db0837b..cf77643f06b 100644 --- a/docs/sources/features/datasources/postgres.md +++ b/docs/sources/features/datasources/postgres.md @@ -68,6 +68,8 @@ Macro example | Description *$__unixEpochFilter(dateColumn)* | Will be replaced by a time range filter using the specified column name with times represented as unix timestamp. For example, *dateColumn >= 1494410783 AND dateColumn <= 1494497183* *$__unixEpochFrom()* | Will be replaced by the start of the currently active time selection as unix timestamp. For example, *1494410783* *$__unixEpochTo()* | Will be replaced by the end of the currently active time selection as unix timestamp. For example, *1494497183* +*$__unixEpochGroup(dateColumn,'5m', [fillmode])* | Same as $__timeGroup but for times stored as unix timestamp (only available in Grafana 5.3+). +*$__unixEpochGroupAlias(dateColumn,'5m', [fillmode])* | Same as above but also adds a column alias (only available in Grafana 5.3+). 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. diff --git a/pkg/tsdb/postgres/macros.go b/pkg/tsdb/postgres/macros.go index a4b4aaa9d1e..d2a3d599441 100644 --- a/pkg/tsdb/postgres/macros.go +++ b/pkg/tsdb/postgres/macros.go @@ -134,6 +134,27 @@ func (m *postgresMacroEngine) evaluateMacro(name string, args []string) (string, return fmt.Sprintf("%d", m.timeRange.GetFromAsSecondsEpoch()), nil case "__unixEpochTo": return fmt.Sprintf("%d", m.timeRange.GetToAsSecondsEpoch()), nil + case "__unixEpochGroup": + if len(args) < 2 { + return "", fmt.Errorf("macro %v needs time column and interval and optional fill value", name) + } + interval, err := time.ParseDuration(strings.Trim(args[1], `'`)) + if err != nil { + return "", fmt.Errorf("error parsing interval %v", args[1]) + } + if len(args) == 3 { + err := tsdb.SetupFillmode(m.query, interval, args[2]) + if err != nil { + return "", err + } + } + return fmt.Sprintf("floor(%s/%v)*%v", args[0], interval.Seconds(), interval.Seconds()), nil + case "__unixEpochGroupAlias": + tg, err := m.evaluateMacro("__unixEpochGroup", args) + if err == nil { + return tg + " AS \"time\"", err + } + return "", err default: return "", fmt.Errorf("Unknown macro %v", name) } diff --git a/pkg/tsdb/postgres/macros_test.go b/pkg/tsdb/postgres/macros_test.go index beeea93893b..a029fc49ee0 100644 --- a/pkg/tsdb/postgres/macros_test.go +++ b/pkg/tsdb/postgres/macros_test.go @@ -110,6 +110,18 @@ func TestMacroEngine(t *testing.T) { So(sql, ShouldEqual, fmt.Sprintf("select %d", to.Unix())) }) + + Convey("interpolate __unixEpochGroup function", func() { + + sql, err := engine.Interpolate(query, timeRange, "SELECT $__unixEpochGroup(time_column,'5m')") + So(err, ShouldBeNil) + sql2, err := engine.Interpolate(query, timeRange, "SELECT $__unixEpochGroupAlias(time_column,'5m')") + So(err, ShouldBeNil) + + So(sql, ShouldEqual, "SELECT floor(time_column/300)*300") + So(sql2, ShouldEqual, sql+" AS \"time\"") + }) + }) Convey("Given a time range between 1960-02-01 07:00 and 1965-02-03 08:00", func() { diff --git a/public/app/plugins/datasource/postgres/partials/query.editor.html b/public/app/plugins/datasource/postgres/partials/query.editor.html index 20353b81ba2..763fd6a6e96 100644 --- a/public/app/plugins/datasource/postgres/partials/query.editor.html +++ b/public/app/plugins/datasource/postgres/partials/query.editor.html @@ -57,6 +57,8 @@ Macros: by setting fillvalue grafana will fill in missing values according to the interval fillvalue can be either a literal value, NULL or previous; previous will fill in the previous seen value or NULL if none has been seen yet - $__timeGroupAlias(column,'5m') -> (extract(epoch from column)/300)::bigint*300 AS "time" +- $__unixEpochGroup(column,'5m') -> floor(column/300)*300 +- $__unixEpochGroupAlias(column,'5m') -> floor(column/300)*300 AS "time" Example of group by and order by with $__timeGroup: SELECT From fbc67a1c64a0a94d169aea63aa00c0f1055dfc6d Mon Sep 17 00:00:00 2001 From: Sven Klemm Date: Mon, 13 Aug 2018 12:17:05 +0200 Subject: [PATCH 383/786] add $__unixEpochGroup to mysql datasource --- docs/sources/features/datasources/mysql.md | 2 ++ pkg/tsdb/mysql/macros.go | 21 +++++++++++++++++++ pkg/tsdb/mysql/macros_test.go | 12 +++++++++++ .../mysql/partials/query.editor.html | 2 ++ 4 files changed, 37 insertions(+) diff --git a/docs/sources/features/datasources/mysql.md b/docs/sources/features/datasources/mysql.md index cdb78deed35..afac746b050 100644 --- a/docs/sources/features/datasources/mysql.md +++ b/docs/sources/features/datasources/mysql.md @@ -71,6 +71,8 @@ Macro example | Description *$__unixEpochFilter(dateColumn)* | Will be replaced by a time range filter using the specified column name with times represented as unix timestamp. For example, *dateColumn > 1494410783 AND dateColumn < 1494497183* *$__unixEpochFrom()* | Will be replaced by the start of the currently active time selection as unix timestamp. For example, *1494410783* *$__unixEpochTo()* | Will be replaced by the end of the currently active time selection as unix timestamp. For example, *1494497183* +*$__unixEpochGroup(dateColumn,'5m', [fillmode])* | Same as $__timeGroup but for times stored as unix timestamp (only available in Grafana 5.3+). +*$__unixEpochGroupAlias(dateColumn,'5m', [fillmode])* | Same as above but also adds a column alias (only available in Grafana 5.3+). 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. diff --git a/pkg/tsdb/mysql/macros.go b/pkg/tsdb/mysql/macros.go index 48fa193edd5..0dabdd7c283 100644 --- a/pkg/tsdb/mysql/macros.go +++ b/pkg/tsdb/mysql/macros.go @@ -112,6 +112,27 @@ func (m *mySqlMacroEngine) evaluateMacro(name string, args []string) (string, er return fmt.Sprintf("%d", m.timeRange.GetFromAsSecondsEpoch()), nil case "__unixEpochTo": return fmt.Sprintf("%d", m.timeRange.GetToAsSecondsEpoch()), nil + case "__unixEpochGroup": + if len(args) < 2 { + return "", fmt.Errorf("macro %v needs time column and interval and optional fill value", name) + } + interval, err := time.ParseDuration(strings.Trim(args[1], `'`)) + if err != nil { + return "", fmt.Errorf("error parsing interval %v", args[1]) + } + if len(args) == 3 { + err := tsdb.SetupFillmode(m.query, interval, args[2]) + if err != nil { + return "", err + } + } + return fmt.Sprintf("%s DIV %v * %v", args[0], interval.Seconds(), interval.Seconds()), nil + case "__unixEpochGroupAlias": + tg, err := m.evaluateMacro("__unixEpochGroup", args) + if err == nil { + return tg + " AS \"time\"", err + } + return "", err default: return "", fmt.Errorf("Unknown macro %v", name) } diff --git a/pkg/tsdb/mysql/macros_test.go b/pkg/tsdb/mysql/macros_test.go index fd9d3f5688a..fe153ca3e2d 100644 --- a/pkg/tsdb/mysql/macros_test.go +++ b/pkg/tsdb/mysql/macros_test.go @@ -97,6 +97,18 @@ func TestMacroEngine(t *testing.T) { So(sql, ShouldEqual, fmt.Sprintf("select %d", to.Unix())) }) + + Convey("interpolate __unixEpochGroup function", func() { + + sql, err := engine.Interpolate(query, timeRange, "SELECT $__unixEpochGroup(time_column,'5m')") + So(err, ShouldBeNil) + sql2, err := engine.Interpolate(query, timeRange, "SELECT $__unixEpochGroupAlias(time_column,'5m')") + So(err, ShouldBeNil) + + So(sql, ShouldEqual, "SELECT time_column DIV 300 * 300") + So(sql2, ShouldEqual, sql+" AS \"time\"") + }) + }) Convey("Given a time range between 1960-02-01 07:00 and 1965-02-03 08:00", func() { diff --git a/public/app/plugins/datasource/mysql/partials/query.editor.html b/public/app/plugins/datasource/mysql/partials/query.editor.html index 7c799eec21b..1e829a1175d 100644 --- a/public/app/plugins/datasource/mysql/partials/query.editor.html +++ b/public/app/plugins/datasource/mysql/partials/query.editor.html @@ -57,6 +57,8 @@ Macros: by setting fillvalue grafana will fill in missing values according to the interval fillvalue can be either a literal value, NULL or previous; previous will fill in the previous seen value or NULL if none has been seen yet - $__timeGroupAlias(column,'5m') -> cast(cast(UNIX_TIMESTAMP(column)/(300) as signed)*300 as signed) AS "time" +- $__unixEpochGroup(column,'5m') -> column DIV 300 * 300 +- $__unixEpochGroupAlias(column,'5m') -> column DIV 300 * 300 AS "time" Example of group by and order by with $__timeGroup: SELECT From 8c4d59363e6aabd9cb772af41569f13e64951691 Mon Sep 17 00:00:00 2001 From: Sven Klemm Date: Mon, 13 Aug 2018 12:23:42 +0200 Subject: [PATCH 384/786] add $__unixEpochGroup to mssql datasource --- docs/sources/features/datasources/mssql.md | 2 ++ pkg/tsdb/mssql/macros.go | 21 +++++++++++++++++++ pkg/tsdb/mssql/macros_test.go | 12 +++++++++++ .../mssql/partials/query.editor.html | 2 ++ 4 files changed, 37 insertions(+) diff --git a/docs/sources/features/datasources/mssql.md b/docs/sources/features/datasources/mssql.md index caaf5a6b321..da0c9581e99 100644 --- a/docs/sources/features/datasources/mssql.md +++ b/docs/sources/features/datasources/mssql.md @@ -88,6 +88,8 @@ Macro example | Description *$__unixEpochFilter(dateColumn)* | Will be replaced by a time range filter using the specified column name with times represented as unix timestamp. For example, *dateColumn > 1494410783 AND dateColumn < 1494497183* *$__unixEpochFrom()* | Will be replaced by the start of the currently active time selection as unix timestamp. For example, *1494410783* *$__unixEpochTo()* | Will be replaced by the end of the currently active time selection as unix timestamp. For example, *1494497183* +*$__unixEpochGroup(dateColumn,'5m', [fillmode])* | Same as $__timeGroup but for times stored as unix timestamp (only available in Grafana 5.3+). +*$__unixEpochGroupAlias(dateColumn,'5m', [fillmode])* | Same as above but also adds a column alias (only available in Grafana 5.3+). 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. diff --git a/pkg/tsdb/mssql/macros.go b/pkg/tsdb/mssql/macros.go index 920e3781e0c..caba043e7b6 100644 --- a/pkg/tsdb/mssql/macros.go +++ b/pkg/tsdb/mssql/macros.go @@ -116,6 +116,27 @@ func (m *msSqlMacroEngine) evaluateMacro(name string, args []string) (string, er return fmt.Sprintf("%d", m.timeRange.GetFromAsSecondsEpoch()), nil case "__unixEpochTo": return fmt.Sprintf("%d", m.timeRange.GetToAsSecondsEpoch()), nil + case "__unixEpochGroup": + if len(args) < 2 { + return "", fmt.Errorf("macro %v needs time column and interval and optional fill value", name) + } + interval, err := time.ParseDuration(strings.Trim(args[1], `'`)) + if err != nil { + return "", fmt.Errorf("error parsing interval %v", args[1]) + } + if len(args) == 3 { + err := tsdb.SetupFillmode(m.query, interval, args[2]) + if err != nil { + return "", err + } + } + return fmt.Sprintf("FLOOR(%s/%v)*%v", args[0], interval.Seconds(), interval.Seconds()), nil + case "__unixEpochGroupAlias": + tg, err := m.evaluateMacro("__unixEpochGroup", args) + if err == nil { + return tg + " AS [time]", err + } + return "", err default: return "", fmt.Errorf("Unknown macro %v", name) } diff --git a/pkg/tsdb/mssql/macros_test.go b/pkg/tsdb/mssql/macros_test.go index 8362ae05aa6..8e0973b750c 100644 --- a/pkg/tsdb/mssql/macros_test.go +++ b/pkg/tsdb/mssql/macros_test.go @@ -145,6 +145,18 @@ func TestMacroEngine(t *testing.T) { So(sql, ShouldEqual, fmt.Sprintf("select %d", to.Unix())) }) + + Convey("interpolate __unixEpochGroup function", func() { + + sql, err := engine.Interpolate(query, timeRange, "SELECT $__unixEpochGroup(time_column,'5m')") + So(err, ShouldBeNil) + sql2, err := engine.Interpolate(query, timeRange, "SELECT $__unixEpochGroupAlias(time_column,'5m')") + So(err, ShouldBeNil) + + So(sql, ShouldEqual, "SELECT FLOOR(time_column/300)*300") + So(sql2, ShouldEqual, sql+" AS [time]") + }) + }) Convey("Given a time range between 1960-02-01 07:00 and 1965-02-03 08:00", func() { diff --git a/public/app/plugins/datasource/mssql/partials/query.editor.html b/public/app/plugins/datasource/mssql/partials/query.editor.html index 7888e36a24c..4b0a46b6412 100644 --- a/public/app/plugins/datasource/mssql/partials/query.editor.html +++ b/public/app/plugins/datasource/mssql/partials/query.editor.html @@ -57,6 +57,8 @@ Macros: by setting fillvalue grafana will fill in missing values according to the interval fillvalue can be either a literal value, NULL or previous; previous will fill in the previous seen value or NULL if none has been seen yet - $__timeGroupAlias(column, '5m'[, fillvalue]) -> CAST(ROUND(DATEDIFF(second, '1970-01-01', column)/300.0, 0) as bigint)*300 AS [time] +- $__unixEpochGroup(column,'5m') -> FLOOR(column/300)*300 +- $__unixEpochGroupAlias(column,'5m') -> FLOOR(column/300)*300 AS [time] Example of group by and order by with $__timeGroup: SELECT From 978e89657ecd4f8795721db2b9c21ea2ab1a0655 Mon Sep 17 00:00:00 2001 From: David Kaltschmidt Date: Mon, 13 Aug 2018 12:53:12 +0200 Subject: [PATCH 385/786] Explore: Fix label filtering for rate queries - exclude `]` from match expression for selector injection to ignore range vectors like `[10m]` --- public/app/plugins/datasource/prometheus/datasource.ts | 2 +- .../app/plugins/datasource/prometheus/specs/datasource.jest.ts | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/public/app/plugins/datasource/prometheus/datasource.ts b/public/app/plugins/datasource/prometheus/datasource.ts index 318b0f8f1fc..9d4d0433d5d 100644 --- a/public/app/plugins/datasource/prometheus/datasource.ts +++ b/public/app/plugins/datasource/prometheus/datasource.ts @@ -39,7 +39,7 @@ export function addLabelToQuery(query: string, key: string, value: string): stri // Add empty selector to bare metric name let previousWord; - query = query.replace(/(\w+)\b(?![\({=",])/g, (match, word, offset) => { + query = query.replace(/(\w+)\b(?![\(\]{=",])/g, (match, word, offset) => { // Check if inside a selector const nextSelectorStart = query.slice(offset).indexOf('{'); const nextSelectorEnd = query.slice(offset).indexOf('}'); diff --git a/public/app/plugins/datasource/prometheus/specs/datasource.jest.ts b/public/app/plugins/datasource/prometheus/specs/datasource.jest.ts index 4ba2e3260a7..ed467c54b24 100644 --- a/public/app/plugins/datasource/prometheus/specs/datasource.jest.ts +++ b/public/app/plugins/datasource/prometheus/specs/datasource.jest.ts @@ -351,6 +351,7 @@ describe('PrometheusDatasource', () => { expect(addLabelToQuery('foo{instance="my-host.com:9100"}', 'bar', 'baz')).toBe( 'foo{bar="baz",instance="my-host.com:9100"}' ); + expect(addLabelToQuery('rate(metric[1m])', 'foo', 'bar')).toBe('rate(metric{foo="bar"}[1m])'); }); }); From 2e2de38b31918f704a0e76ec60e5d997e2ed0bb1 Mon Sep 17 00:00:00 2001 From: Tobias Skarhed Date: Mon, 13 Aug 2018 13:55:47 +0200 Subject: [PATCH 386/786] Mock things --- public/app/plugins/panel/heatmap/rendering.ts | 2 +- .../panel/heatmap/specs/renderer.jest.ts | 39 ++++++++++++++----- 2 files changed, 30 insertions(+), 11 deletions(-) diff --git a/public/app/plugins/panel/heatmap/rendering.ts b/public/app/plugins/panel/heatmap/rendering.ts index 5af916ac13e..e68d63cfbf8 100644 --- a/public/app/plugins/panel/heatmap/rendering.ts +++ b/public/app/plugins/panel/heatmap/rendering.ts @@ -456,7 +456,6 @@ export class Link { this.chartHeight = this.height - this.margin.top - this.margin.bottom; this.chartTop = this.margin.top; this.chartBottom = this.chartTop + this.chartHeight; - if (this.panel.dataFormat === 'tsbuckets') { this.addYAxisFromBuckets(); } else { @@ -550,6 +549,7 @@ export class Link { .style('opacity', this.getCardOpacity.bind(this)); let $cards = this.$heatmap.find('.heatmap-card'); + console.log($cards); $cards .on('mouseenter', event => { this.tooltip.mouseOverBucket = true; diff --git a/public/app/plugins/panel/heatmap/specs/renderer.jest.ts b/public/app/plugins/panel/heatmap/specs/renderer.jest.ts index c660761890c..a5546624d65 100644 --- a/public/app/plugins/panel/heatmap/specs/renderer.jest.ts +++ b/public/app/plugins/panel/heatmap/specs/renderer.jest.ts @@ -8,17 +8,24 @@ import TimeSeries from 'app/core/time_series2'; import moment from 'moment'; // import { Emitter } from 'app/core/core'; import rendering from '../rendering'; +// import * as d3 from 'd3'; import { convertToHeatMap, convertToCards, histogramToHeatmap, calculateBucketSize } from '../heatmap_data_converter'; jest.mock('app/core/core', () => ({ appEvents: { on: () => {}, }, + contextSrv: { + user: { + lightTheme: false, + }, + }, })); describe('grafanaHeatmap', function() { // beforeEach(angularMocks.module('grafana.core')); let scope = {}; + let render; function heatmapScenario(desc, func, elementWidth = 500) { describe(desc, function() { @@ -154,11 +161,20 @@ describe('grafanaHeatmap', function() { ctrl.data = ctx.data; ctx.element = { - find: () => ({ on: () => {} }), + find: () => ({ + on: () => {}, + css: () => 189, + width: () => 189, + height: () => 200, + find: () => ({ + on: () => {}, + }), + }), on: () => {}, }; - rendering(scope, ctx.element, [], ctrl); - ctrl.events.emit('render'); + render = rendering(scope, ctx.element, [], ctrl); + render.render(); + render.ctrl.renderingCompleted(); }); }; @@ -172,6 +188,9 @@ describe('grafanaHeatmap', function() { }); it('should draw correct Y axis', function() { + console.log('Runnign first test'); + // console.log(render.ctrl.data); + console.log(render.scope.yScale); var yTicks = getTicks(ctx.element, '.axis-y'); expect(yTicks).toEqual(['1', '2', '3']); }); @@ -317,13 +336,13 @@ describe('grafanaHeatmap', function() { }); function getTicks(element, axisSelector) { - return element - .find(axisSelector) - .find('text') - .map(function() { - return this.textContent; - }) - .get(); + // return element + // .find(axisSelector) + // .find('text') + // .map(function() { + // return this.textContent; + // }) + // .get(); } function formatTime(timeStr) { From e6057e08de4cddf5ba1a9f6c163f66835a15161b Mon Sep 17 00:00:00 2001 From: Tobias Skarhed Date: Mon, 13 Aug 2018 14:24:15 +0200 Subject: [PATCH 387/786] Rename to HeatmapRenderer --- public/app/plugins/panel/heatmap/rendering.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/public/app/plugins/panel/heatmap/rendering.ts b/public/app/plugins/panel/heatmap/rendering.ts index e68d63cfbf8..6d3d21420e0 100644 --- a/public/app/plugins/panel/heatmap/rendering.ts +++ b/public/app/plugins/panel/heatmap/rendering.ts @@ -20,9 +20,9 @@ let MIN_CARD_SIZE = 1, MIN_SELECTION_WIDTH = 2; export default function rendering(scope, elem, attrs, ctrl) { - return new Link(scope, elem, attrs, ctrl); + return new HeatmapRenderer(scope, elem, attrs, ctrl); } -export class Link { +export class HeatmapRenderer { width: number; height: number; yScale: any; From 535bab1baaf45288e863fb04e89974f37b359421 Mon Sep 17 00:00:00 2001 From: Patrick O'Carroll Date: Mon, 13 Aug 2018 15:07:29 +0200 Subject: [PATCH 388/786] now hides team header when no teams + fix for list hidden when only one team --- public/app/features/org/partials/profile.html | 2 +- public/app/features/org/profile_ctrl.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/public/app/features/org/partials/profile.html b/public/app/features/org/partials/profile.html index b204c223138..7858e00c683 100644 --- a/public/app/features/org/partials/profile.html +++ b/public/app/features/org/partials/profile.html @@ -26,7 +26,7 @@ -

    Teams

    +

    Teams

    diff --git a/public/app/features/org/profile_ctrl.ts b/public/app/features/org/profile_ctrl.ts index 6cfcdc2e64c..40ee4d908a1 100644 --- a/public/app/features/org/profile_ctrl.ts +++ b/public/app/features/org/profile_ctrl.ts @@ -30,7 +30,7 @@ export class ProfileCtrl { getUserTeams() { this.backendSrv.get('/api/user/teams').then(teams => { this.teams = teams; - this.showTeamsList = this.teams.length > 1; + this.showTeamsList = this.teams.length > 0; }); } From fd032c11111833bba562966a6379c4e20c102da6 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Mon, 13 Aug 2018 15:18:33 +0200 Subject: [PATCH 389/786] changelog: add notes about closing #12476 [skip ci] --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7d5ed3378de..0a36943af65 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ * **LDAP**: Define Grafana Admin permission in ldap group mappings [#2469](https://github.com/grafana/grafana/issues/2496), PR [#12622](https://github.com/grafana/grafana/issues/12622) * **Cloudwatch**: CloudWatch GetMetricData support [#11487](https://github.com/grafana/grafana/issues/11487), thx [@mtanda](https://github.com/mtanda) * **Configuration**: Allow auto-assigning users to specific organization (other than Main. Org) [#1823](https://github.com/grafana/grafana/issues/1823) [#12801](https://github.com/grafana/grafana/issues/12801), thx [@gzzo](https://github.com/gzzo) and [@ofosos](https://github.com/ofosos) +* **Profile**: List teams that the user is member of in current/active organization [#12476](https://github.com/grafana/grafana/issues/12476) ### Minor From f2b1fabd5c142d48f93f2499316d9a898fd09a0b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 13 Aug 2018 15:38:28 +0200 Subject: [PATCH 390/786] fix: Alerting rendering timeout was 30 seconds, same as alert rule eval timeout, this should be much lower so the rendering timeout does not timeout the rule context, fixes #12151 (#12903) --- pkg/services/alerting/notifier.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pkg/services/alerting/notifier.go b/pkg/services/alerting/notifier.go index 07212746f7e..f4e0a0f434f 100644 --- a/pkg/services/alerting/notifier.go +++ b/pkg/services/alerting/notifier.go @@ -3,7 +3,6 @@ package alerting import ( "errors" "fmt" - "time" "golang.org/x/sync/errgroup" @@ -81,7 +80,7 @@ func (n *notificationService) uploadImage(context *EvalContext) (err error) { renderOpts := rendering.Opts{ Width: 1000, Height: 500, - Timeout: time.Second * 30, + Timeout: alertTimeout / 2, OrgId: context.Rule.OrgId, OrgRole: m.ROLE_ADMIN, } From b8a1385c77fd15ce7a15c3be956334f20f4de339 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Mon, 13 Aug 2018 15:38:37 +0200 Subject: [PATCH 391/786] build: increase frontend tests timeout without no output --- .circleci/config.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.circleci/config.yml b/.circleci/config.yml index 8f2e9b6c1af..977121c30ee 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -104,6 +104,7 @@ jobs: - run: name: yarn install command: 'yarn install --pure-lockfile --no-progress' + no_output_timeout: 15m - save_cache: key: dependency-cache-{{ checksum "yarn.lock" }} paths: From b0f3ca16d9acc839560619284613814f4fcb3797 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 13 Aug 2018 15:40:37 +0200 Subject: [PATCH 392/786] Update CHANGELOG.md --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0a36943af65..6eea9bb7337 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,7 @@ * **Postgres**: Escape ssl mode parameter in connectionstring [#12644](https://github.com/grafana/grafana/issues/12644), thx [@yogyrahmawan](https://github.com/yogyrahmawan) * **Github OAuth**: Allow changes of user info at Github to be synched to Grafana when signing in [#11818](https://github.com/grafana/grafana/issues/11818), thx [@rwaweber](https://github.com/rwaweber) * **Alerting**: Fix diff and percent_diff reducers [#11563](https://github.com/grafana/grafana/issues/11563), thx [@jessetane](https://github.com/jessetane) +* **Alerting**: Fix rendering timeout which could cause notifications to not be sent due to rendering timing out [#12151](https://github.com/grafana/grafana/issues/12151) * **Units**: Polish złoty currency [#12691](https://github.com/grafana/grafana/pull/12691), thx [@mwegrzynek](https://github.com/mwegrzynek) * **Cloudwatch**: Improved error handling [#12489](https://github.com/grafana/grafana/issues/12489), thx [@mtanda](https://github.com/mtanda) * **Cloudwatch**: AppSync metrics and dimensions [#12300](https://github.com/grafana/grafana/issues/12300), thx [@franciscocpg](https://github.com/franciscocpg) From 1c185ef8d824158765ecb2919c772a68876ecc74 Mon Sep 17 00:00:00 2001 From: David Date: Mon, 13 Aug 2018 15:40:52 +0200 Subject: [PATCH 393/786] Add commit to external stylesheet url (#12902) - currently only the release is used as a fingerprint which produces caching issues for all lastest master builds - also add build commit to url fingerprint - make bra also watch go html template files --- .bra.toml | 2 +- public/views/index.template.html | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.bra.toml b/.bra.toml index dcf316466d6..15961e1e3fd 100644 --- a/.bra.toml +++ b/.bra.toml @@ -9,7 +9,7 @@ watch_dirs = [ "$WORKDIR/public/views", "$WORKDIR/conf", ] -watch_exts = [".go", ".ini", ".toml"] +watch_exts = [".go", ".ini", ".toml", ".template.html"] build_delay = 1500 cmds = [ ["go", "run", "build.go", "-dev", "build-server"], diff --git a/public/views/index.template.html b/public/views/index.template.html index ae35666b189..f4c5d183fc8 100644 --- a/public/views/index.template.html +++ b/public/views/index.template.html @@ -11,7 +11,7 @@ - + @@ -107,12 +107,12 @@ [[end]] - + \ No newline at end of file From 39669e5002207fd0b486eeb49a0fa417b51a1e09 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Mon, 13 Aug 2018 15:41:15 +0200 Subject: [PATCH 394/786] fix redirect to panel when using an outdated dashboard slug (#12901) --- public/app/routes/dashboard_loaders.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/public/app/routes/dashboard_loaders.ts b/public/app/routes/dashboard_loaders.ts index 3642b54c790..b33d5b6afb1 100644 --- a/public/app/routes/dashboard_loaders.ts +++ b/public/app/routes/dashboard_loaders.ts @@ -34,7 +34,9 @@ export class LoadDashboardCtrl { const url = locationUtil.stripBaseFromUrl(result.meta.url); if (url !== $location.path()) { + // replace url to not create additional history items and then return so that initDashboard below isn't executed multiple times. $location.path(url).replace(); + return; } } From 9031866caaa64b71a38395815985b715e821582e Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Mon, 13 Aug 2018 15:51:19 +0200 Subject: [PATCH 395/786] changelog: add notes about closing #12805 [skip ci] --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6eea9bb7337..efc7e44d31b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ * **Cloudwatch**: CloudWatch GetMetricData support [#11487](https://github.com/grafana/grafana/issues/11487), thx [@mtanda](https://github.com/mtanda) * **Configuration**: Allow auto-assigning users to specific organization (other than Main. Org) [#1823](https://github.com/grafana/grafana/issues/1823) [#12801](https://github.com/grafana/grafana/issues/12801), thx [@gzzo](https://github.com/gzzo) and [@ofosos](https://github.com/ofosos) * **Profile**: List teams that the user is member of in current/active organization [#12476](https://github.com/grafana/grafana/issues/12476) +* **LDAP**: Client certificates support [#12805](https://github.com/grafana/grafana/issues/12805), thx [@nyxi](https://github.com/nyxi) ### Minor From 472b880939c98716de1ad5f654bb99e79aa11627 Mon Sep 17 00:00:00 2001 From: Tobias Skarhed Date: Mon, 13 Aug 2018 15:51:58 +0200 Subject: [PATCH 396/786] Add React container --- .../panel/heatmap/HeatmapRenderContainer.tsx | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 public/app/plugins/panel/heatmap/HeatmapRenderContainer.tsx diff --git a/public/app/plugins/panel/heatmap/HeatmapRenderContainer.tsx b/public/app/plugins/panel/heatmap/HeatmapRenderContainer.tsx new file mode 100644 index 00000000000..e5982a485ca --- /dev/null +++ b/public/app/plugins/panel/heatmap/HeatmapRenderContainer.tsx @@ -0,0 +1,20 @@ +import React from 'react'; +import HeatmapRenderer from './rendering'; +import { HeatmapCtrl } from './heatmap_ctrl'; + +export class HeatmapRenderContainer extends React.Component { + renderer: any; + constructor(props) { + super(props); + this.renderer = HeatmapRenderer( + this.props.scope, + this.props.children[0], + [], + new HeatmapCtrl(this.props.scope, {}, {}) + ); + } + + render() { + return
    ; + } +} From c521f51780b12937cdc1c9c844f92d9515190320 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 13 Aug 2018 15:56:11 +0200 Subject: [PATCH 397/786] tech: removed js related stuff now that 99% is typescript (#12905) --- .jscs.json | 13 -- .jshintrc | 37 ---- Gruntfile.js | 1 - package.json | 3 - scripts/grunt/default_task.js | 4 - scripts/grunt/options/jscs.js | 22 -- scripts/grunt/options/jshint.js | 20 -- tasks/options/copy.js | 45 ---- yarn.lock | 368 +++----------------------------- 9 files changed, 28 insertions(+), 485 deletions(-) delete mode 100644 .jscs.json delete mode 100644 .jshintrc delete mode 100644 scripts/grunt/options/jscs.js delete mode 100644 scripts/grunt/options/jshint.js delete mode 100644 tasks/options/copy.js diff --git a/.jscs.json b/.jscs.json deleted file mode 100644 index 8fdad332de5..00000000000 --- a/.jscs.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "disallowImplicitTypeConversion": ["string"], - "disallowKeywords": ["with"], - "disallowMultipleLineBreaks": true, - "disallowMixedSpacesAndTabs": true, - "disallowTrailingWhitespace": true, - "requireSpacesInFunctionExpression": { - "beforeOpeningCurlyBrace": true - }, - "disallowSpacesInsideArrayBrackets": true, - "disallowSpacesInsideParentheses": true, - "validateIndentation": 2 -} diff --git a/.jshintrc b/.jshintrc deleted file mode 100644 index 1d8fad63173..00000000000 --- a/.jshintrc +++ /dev/null @@ -1,37 +0,0 @@ -{ - "browser": true, - "esversion": 6, - "bitwise":false, - "curly": true, - "eqnull": true, - "strict": false, - "devel": true, - "eqeqeq": true, - "forin": false, - "immed": true, - "supernew": true, - "expr": true, - "indent": 2, - "latedef": false, - "newcap": true, - "noarg": true, - "noempty": true, - "undef": true, - "boss": true, - "trailing": true, - "laxbreak": true, - "laxcomma": true, - "sub": true, - "unused": true, - "maxdepth": 6, - "maxlen": 140, - - "globals": { - "System": true, - "Promise": true, - "define": true, - "require": true, - "Chromath": false, - "setImmediate": true - } -} diff --git a/Gruntfile.js b/Gruntfile.js index 23276e8a122..8a71fb44148 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -1,4 +1,3 @@ -/* jshint node:true */ 'use strict'; module.exports = function (grunt) { var os = require('os'); diff --git a/package.json b/package.json index 200285d7a1e..24e23b574df 100644 --- a/package.json +++ b/package.json @@ -45,9 +45,7 @@ "grunt-contrib-concat": "^1.0.1", "grunt-contrib-copy": "~1.0.0", "grunt-contrib-cssmin": "~1.0.2", - "grunt-contrib-jshint": "~1.1.0", "grunt-exec": "^1.0.1", - "grunt-jscs": "3.0.1", "grunt-karma": "~2.0.0", "grunt-notify": "^0.4.5", "grunt-postcss": "^0.8.0", @@ -60,7 +58,6 @@ "html-webpack-plugin": "^3.2.0", "husky": "^0.14.3", "jest": "^22.0.4", - "jshint-stylish": "~2.2.1", "karma": "1.7.0", "karma-chrome-launcher": "~2.2.0", "karma-expect": "~1.1.3", diff --git a/scripts/grunt/default_task.js b/scripts/grunt/default_task.js index 719f0ab4e95..efcdcd02963 100644 --- a/scripts/grunt/default_task.js +++ b/scripts/grunt/default_task.js @@ -9,8 +9,6 @@ module.exports = function(grunt) { ]); grunt.registerTask('test', [ - 'jscs', - 'jshint', 'sasslint', 'exec:tslint', "exec:jest", @@ -19,8 +17,6 @@ module.exports = function(grunt) { ]); grunt.registerTask('precommit', [ - 'jscs', - 'jshint', 'sasslint', 'exec:tslint', 'no-only-tests' diff --git a/scripts/grunt/options/jscs.js b/scripts/grunt/options/jscs.js deleted file mode 100644 index 8296e59a506..00000000000 --- a/scripts/grunt/options/jscs.js +++ /dev/null @@ -1,22 +0,0 @@ -module.exports = function(config) { - return { - src: [ - 'Gruntfile.js', - '<%= srcDir %>/app/**/*.js', - '<%= srcDir %>/plugin/**/*.js', - '!<%= srcDir %>/app/dashboards/*' - ], - options: { - config: ".jscs.json", - }, - }; -}; - -/* - "requireCurlyBraces": ["if", "else", "for", "while", "do", "try", "catch"], - "requireSpaceAfterKeywords": ["if", "else", "for", "while", "do", "switch", "return", "try", "catch"], - "disallowLeftStickedOperators": ["?", "+", "-", "/", "*", "=", "==", "===", "!=", "!==", ">", ">=", "<", "<="], - "disallowRightStickedOperators": ["?", "+", "/", "*", ":", "=", "==", "===", "!=", "!==", ">", ">=", "<", "<="], - "requireRightStickedOperators": ["!"], - "requireLeftStickedOperators": [","], - */ diff --git a/scripts/grunt/options/jshint.js b/scripts/grunt/options/jshint.js deleted file mode 100644 index 7ea36eac3ff..00000000000 --- a/scripts/grunt/options/jshint.js +++ /dev/null @@ -1,20 +0,0 @@ -module.exports = function(config) { - return { - source: { - files: { - src: ['Gruntfile.js', '<%= srcDir %>/app/**/*.js'], - } - }, - options: { - jshintrc: true, - reporter: require('jshint-stylish'), - ignores: [ - 'node_modules/*', - 'dist/*', - 'sample/*', - '<%= srcDir %>/vendor/*', - '<%= srcDir %>/app/dashboards/*' - ] - } - }; -}; diff --git a/tasks/options/copy.js b/tasks/options/copy.js deleted file mode 100644 index 1ef32af6951..00000000000 --- a/tasks/options/copy.js +++ /dev/null @@ -1,45 +0,0 @@ -module.exports = function(config) { - return { - // copy source to temp, we will minify in place for the dist build - everything_but_less_to_temp: { - cwd: '<%= srcDir %>', - expand: true, - src: ['**/*', '!**/*.less'], - dest: '<%= tempDir %>' - }, - - public_to_gen: { - cwd: '<%= srcDir %>', - expand: true, - src: ['**/*', '!**/*.less'], - dest: '<%= genDir %>' - }, - - node_modules: { - cwd: './node_modules', - expand: true, - src: [ - 'ace-builds/src-noconflict/**/*', - 'eventemitter3/*.js', - 'systemjs/dist/*.js', - 'es6-promise/**/*', - 'es6-shim/*.js', - 'reflect-metadata/*.js', - 'reflect-metadata/*.ts', - 'reflect-metadata/*.d.ts', - 'rxjs/**/*', - 'tether/**/*', - 'tether-drop/**/*', - 'tether-drop/**/*', - 'remarkable/dist/*', - 'remarkable/dist/*', - 'virtual-scroll/**/*', - 'mousetrap/**/*', - 'twemoji/2/twemoji.amd*', - 'twemoji/2/svg/*.svg', - ], - dest: '<%= srcDir %>/vendor/npm' - } - - }; -}; diff --git a/yarn.lock b/yarn.lock index ed8a1eabec3..89e74828351 100644 --- a/yarn.lock +++ b/yarn.lock @@ -414,10 +414,6 @@ JSONStream@^1.3.2: jsonparse "^1.2.0" through ">=2.2.7 <3" -JSV@^4.0.x: - version "4.0.2" - resolved "https://registry.yarnpkg.com/JSV/-/JSV-4.0.2.tgz#d077f6825571f82132f9dffaed587b4029feff57" - abab@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/abab/-/abab-1.0.4.tgz#5faad9c2c07f60dd76770f71cf025b62a63cfd4e" @@ -869,10 +865,6 @@ async-limiter@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/async-limiter/-/async-limiter-1.0.0.tgz#78faed8c3d074ab81f22b4e985d79e8738f720f8" -async@0.2.x, async@~0.2.6, async@~0.2.9: - version "0.2.10" - resolved "https://registry.yarnpkg.com/async/-/async-0.2.10.tgz#b6bbe0b0674b9d719708ca38de8c237cb526c3d1" - async@^1.4.0, async@^1.5.0, async@^1.5.2, async@~1.5.2: version "1.5.2" resolved "https://registry.yarnpkg.com/async/-/async-1.5.2.tgz#ec6a61ae56480c0c3cb241c95618e20892f9672a" @@ -883,6 +875,10 @@ async@^2.0.0, async@^2.1.4, async@^2.4.1, async@^2.6.0: dependencies: lodash "^4.17.10" +async@~0.2.6: + version "0.2.10" + resolved "https://registry.yarnpkg.com/async/-/async-0.2.10.tgz#b6bbe0b0674b9d719708ca38de8c237cb526c3d1" + asynckit@^0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" @@ -1564,7 +1560,7 @@ babel-types@^6.18.0, babel-types@^6.19.0, babel-types@^6.24.1, babel-types@^6.26 lodash "^4.17.4" to-fast-properties "^1.0.3" -babylon@^6.17.3, babylon@^6.18.0, babylon@^6.8.1: +babylon@^6.17.3, babylon@^6.18.0: version "6.18.0" resolved "https://registry.yarnpkg.com/babylon/-/babylon-6.18.0.tgz#af2f3b88fa6f5c1e4c634d1a0f8eac4f55b395e3" @@ -1626,10 +1622,6 @@ bcrypt-pbkdf@^1.0.0: dependencies: tweetnacl "^0.14.3" -beeper@^1.1.0: - version "1.1.1" - resolved "https://registry.yarnpkg.com/beeper/-/beeper-1.1.1.tgz#e6d5ea8c5dad001304a70b22638447f69cb2f809" - better-assert@~1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/better-assert/-/better-assert-1.0.2.tgz#40866b9e1b9e0b55b481894311e68faffaebc522" @@ -2109,7 +2101,7 @@ center-align@^0.1.1: align-text "^0.1.3" lazy-cache "^1.0.3" -chalk@^1.0.0, chalk@^1.1.1, chalk@^1.1.3, chalk@~1.1.0, chalk@~1.1.1: +chalk@^1.0.0, chalk@^1.1.1, chalk@^1.1.3, chalk@~1.1.1: version "1.1.3" resolved "https://registry.yarnpkg.com/chalk/-/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98" dependencies: @@ -2315,7 +2307,7 @@ cli-table2@^0.2.0, cli-table2@~0.2.0: optionalDependencies: colors "^1.1.2" -cli-table@^0.3.1, cli-table@~0.3.1: +cli-table@^0.3.1: version "0.3.1" resolved "https://registry.yarnpkg.com/cli-table/-/cli-table-0.3.1.tgz#f53b05266a8b1a0b934b3d0821e6e2dc5914ae23" dependencies: @@ -2332,13 +2324,6 @@ cli-width@^2.0.0: version "2.2.0" resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-2.2.0.tgz#ff19ede8a9a5e579324147b0c11f0fbcbabed639" -cli@~1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/cli/-/cli-1.0.1.tgz#22817534f24bfa4950c34d532d48ecbc621b8c14" - dependencies: - exit "0.1.2" - glob "^7.1.1" - clipboard@^1.7.1: version "1.7.1" resolved "https://registry.yarnpkg.com/clipboard/-/clipboard-1.7.1.tgz#360d6d6946e99a7a1fef395e42ba92b5e9b5a16b" @@ -2490,10 +2475,6 @@ colors@0.5.x: version "0.5.1" resolved "https://registry.yarnpkg.com/colors/-/colors-0.5.1.tgz#7d0023eaeb154e8ee9fce75dcb923d0ed1667774" -colors@0.6.x: - version "0.6.2" - resolved "https://registry.yarnpkg.com/colors/-/colors-0.6.2.tgz#2423fe6678ac0c5dae8852e5d0e5be08c997abcc" - colors@1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/colors/-/colors-1.0.3.tgz#0433f44d809680fdeb60ed260f1b0c262e82a40b" @@ -2539,7 +2520,7 @@ commander@2.8.x: dependencies: graceful-readlink ">= 1.0.0" -commander@2.9.x, commander@~2.9.0: +commander@2.9.x: version "2.9.0" resolved "https://registry.yarnpkg.com/commander/-/commander-2.9.0.tgz#9c99094176e12240cb22d6c5146098400fe0f7d4" dependencies: @@ -2549,12 +2530,6 @@ commander@~2.13.0: version "2.13.0" resolved "https://registry.yarnpkg.com/commander/-/commander-2.13.0.tgz#6964bca67685df7c1f1430c584f07d7597885b9c" -comment-parser@^0.3.1: - version "0.3.2" - resolved "https://registry.yarnpkg.com/comment-parser/-/comment-parser-0.3.2.tgz#3c03f0776b86a36dfd9a0a2c97c6307f332082fe" - dependencies: - readable-stream "^2.0.4" - commondir@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b" @@ -2660,7 +2635,7 @@ connect@^3.6.0: parseurl "~1.3.2" utils-merge "1.0.1" -console-browserify@1.1.x, console-browserify@^1.1.0: +console-browserify@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/console-browserify/-/console-browserify-1.1.0.tgz#f0241c45730a9fc6323b206dbf38edc741d0bb10" dependencies: @@ -2978,14 +2953,6 @@ csstype@^2.2.0: version "2.5.3" resolved "https://registry.yarnpkg.com/csstype/-/csstype-2.5.3.tgz#2504152e6e1cc59b32098b7f5d6a63f16294c1f7" -cst@^0.4.3: - version "0.4.10" - resolved "https://registry.yarnpkg.com/cst/-/cst-0.4.10.tgz#9c05c825290a762f0a85c0aabb8c0fe035ae8516" - dependencies: - babel-runtime "^6.9.2" - babylon "^6.8.1" - source-map-support "^0.4.0" - currently-unhandled@^0.4.1: version "0.4.1" resolved "https://registry.yarnpkg.com/currently-unhandled/-/currently-unhandled-0.4.1.tgz#988df33feab191ef799a61369dd76c17adf957ea" @@ -2996,10 +2963,6 @@ custom-event@~1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/custom-event/-/custom-event-1.0.1.tgz#5d02a46850adf1b4a317946a3928fccb5bfd0425" -cycle@1.0.x: - version "1.0.3" - resolved "https://registry.yarnpkg.com/cycle/-/cycle-1.0.3.tgz#21e80b2be8580f98b468f379430662b046c34ad2" - cyclist@~0.2.2: version "0.2.2" resolved "https://registry.yarnpkg.com/cyclist/-/cyclist-0.2.2.tgz#1b33792e11e914a2fd6d6ed6447464444e5fa640" @@ -3324,7 +3287,7 @@ dedent@^0.7.0: version "0.7.0" resolved "https://registry.yarnpkg.com/dedent/-/dedent-0.7.0.tgz#2495ddbaf6eb874abb0e1be9df22d2e5a544326c" -deep-equal@*, deep-equal@^1.0.1: +deep-equal@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-1.0.1.tgz#f5d260292b660e084eff4cdbc9f08ad3247448b5" @@ -3604,12 +3567,6 @@ domhandler@2.1: dependencies: domelementtype "1" -domhandler@2.3: - version "2.3.0" - resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-2.3.0.tgz#2de59a0822d5027fabff6f032c2b25a2a8abe738" - dependencies: - domelementtype "1" - domhandler@^2.3.0: version "2.4.2" resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-2.4.2.tgz#8805097e933d65e85546f726d60f5eb88b44f803" @@ -3622,7 +3579,7 @@ domutils@1.1: dependencies: domelementtype "1" -domutils@1.5, domutils@1.5.1: +domutils@1.5.1: version "1.5.1" resolved "https://registry.yarnpkg.com/domutils/-/domutils-1.5.1.tgz#dcd8488a26f563d61079e48c9f7b7e32373682cf" dependencies: @@ -3813,10 +3770,6 @@ ent@~2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/ent/-/ent-2.2.0.tgz#e964219325a21d05f44466a2f686ed6ce5f5dd1d" -entities@1.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/entities/-/entities-1.0.0.tgz#b2987aa3821347fcde642b24fdfc9e4fb712bf26" - entities@^1.1.1, entities@~1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/entities/-/entities-1.1.1.tgz#6e5c2d0a5621b5dadaecef80b90edfb5cd7772f0" @@ -4181,7 +4134,7 @@ exit-hook@^1.0.0: version "1.1.1" resolved "https://registry.yarnpkg.com/exit-hook/-/exit-hook-1.1.1.tgz#f05ca233b48c05d54fff07765df8507e95c02ff8" -exit@0.1.2, exit@0.1.x, exit@^0.1.2, exit@~0.1.1, exit@~0.1.2: +exit@^0.1.2, exit@~0.1.1: version "0.1.2" resolved "https://registry.yarnpkg.com/exit/-/exit-0.1.2.tgz#0632638f8d877cc82107d30a0fff1a17cba1cd0c" @@ -4362,10 +4315,6 @@ extsprintf@^1.2.0: version "1.4.0" resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.4.0.tgz#e2689f8f356fad62cca65a3a91c5df5f9551692f" -eyes@0.1.x: - version "0.1.8" - resolved "https://registry.yarnpkg.com/eyes/-/eyes-0.1.8.tgz#62cf120234c683785d902348a800ef3e0cc20bc0" - fast-deep-equal@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-1.1.0.tgz#c053477817c86b51daa853c81e059b733d023614" @@ -4945,9 +4894,9 @@ glob@7.1.2, glob@^7.0.0, glob@^7.0.3, glob@^7.0.5, glob@^7.1.1, glob@^7.1.2, glo once "^1.3.0" path-is-absolute "^1.0.0" -glob@^5.0.1, glob@~5.0.0: - version "5.0.15" - resolved "https://registry.yarnpkg.com/glob/-/glob-5.0.15.tgz#1bc936b9e02f4a603fcc222ecf7633d30b8b93b1" +glob@^6.0.4: + version "6.0.4" + resolved "https://registry.yarnpkg.com/glob/-/glob-6.0.4.tgz#0f08860f6a155127b2fadd4f9ce24b1aab6e4d22" dependencies: inflight "^1.0.4" inherits "2" @@ -4955,9 +4904,9 @@ glob@^5.0.1, glob@~5.0.0: once "^1.3.0" path-is-absolute "^1.0.0" -glob@^6.0.4: - version "6.0.4" - resolved "https://registry.yarnpkg.com/glob/-/glob-6.0.4.tgz#0f08860f6a155127b2fadd4f9ce24b1aab6e4d22" +glob@~5.0.0: + version "5.0.15" + resolved "https://registry.yarnpkg.com/glob/-/glob-5.0.15.tgz#1bc936b9e02f4a603fcc222ecf7633d30b8b93b1" dependencies: inflight "^1.0.4" inherits "2" @@ -5199,27 +5148,10 @@ grunt-contrib-cssmin@~1.0.2: clean-css "~3.4.2" maxmin "^1.1.0" -grunt-contrib-jshint@~1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/grunt-contrib-jshint/-/grunt-contrib-jshint-1.1.0.tgz#369d909b2593c40e8be79940b21340850c7939ac" - dependencies: - chalk "^1.1.1" - hooker "^0.2.3" - jshint "~2.9.4" - grunt-exec@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/grunt-exec/-/grunt-exec-1.0.1.tgz#e5d53a39c5f346901305edee5c87db0f2af999c4" -grunt-jscs@3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/grunt-jscs/-/grunt-jscs-3.0.1.tgz#1fae50e3e955df9e3a9d9425aec22accae008092" - dependencies: - hooker "~0.2.3" - jscs "~3.0.5" - lodash "~4.6.1" - vow "~0.4.1" - grunt-karma@~2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/grunt-karma/-/grunt-karma-2.0.0.tgz#753583d115dfdc055fe57e58f96d6b3c7e612118" @@ -5530,7 +5462,7 @@ homedir-polyfill@^1.0.1: dependencies: parse-passwd "^1.0.0" -hooker@^0.2.3, hooker@~0.2.3: +hooker@~0.2.3: version "0.2.3" resolved "https://registry.yarnpkg.com/hooker/-/hooker-0.2.3.tgz#b834f723cc4a242aa65963459df6d984c5d3d959" @@ -5614,16 +5546,6 @@ html-webpack-plugin@^3.2.0: toposort "^1.0.0" util.promisify "1.0.0" -htmlparser2@3.8.3, htmlparser2@3.8.x: - version "3.8.3" - resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-3.8.3.tgz#996c28b191516a8be86501a7d79757e5c70c1068" - dependencies: - domelementtype "1" - domhandler "2.3" - domutils "1.5" - entities "1.0" - readable-stream "1.1" - htmlparser2@^3.9.1: version "3.9.2" resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-3.9.2.tgz#1bdf87acca0f3f9e53fa4fcceb0f4b4cbb00b338" @@ -5739,10 +5661,6 @@ husky@^0.14.3: normalize-path "^1.0.0" strip-indent "^2.0.0" -i@0.3.x: - version "0.3.6" - resolved "https://registry.yarnpkg.com/i/-/i-0.3.6.tgz#d96c92732076f072711b6b10fd7d4f65ad8ee23d" - iconv-lite@0.4, iconv-lite@0.4.23, iconv-lite@^0.4.17, iconv-lite@^0.4.4, iconv-lite@~0.4.13: version "0.4.23" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.23.tgz#297871f63be507adcfbfca715d0cd0eed84e9a63" @@ -5838,10 +5756,6 @@ inflight@^1.0.4, inflight@~1.0.6: once "^1.3.0" wrappy "1" -inherit@^2.2.2: - version "2.2.6" - resolved "https://registry.yarnpkg.com/inherit/-/inherit-2.2.6.tgz#f1614b06c8544e8128e4229c86347db73ad9788d" - inherits@2, inherits@2.0.3, inherits@^2.0.1, inherits@^2.0.3, inherits@~2.0.0, inherits@~2.0.1, inherits@~2.0.3: version "2.0.3" resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" @@ -5938,10 +5852,6 @@ ipaddr.js@1.6.0: version "1.6.0" resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.6.0.tgz#e3fa357b773da619f26e95f049d055c72796f86b" -irregular-plurals@^1.0.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/irregular-plurals/-/irregular-plurals-1.4.0.tgz#2ca9b033651111855412f16be5d77c62a458a766" - is-absolute-url@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/is-absolute-url/-/is-absolute-url-2.1.0.tgz#50530dfb84fcc9aa7dbe7852e83a37b93b9f2aa6" @@ -6348,7 +6258,7 @@ isomorphic-fetch@^2.1.1: node-fetch "^1.0.1" whatwg-fetch ">=0.10.0" -isstream@0.1.x, isstream@~0.1.2: +isstream@~0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a" @@ -6748,14 +6658,6 @@ js-yaml@^3.4.3, js-yaml@^3.4.6, js-yaml@^3.5.1, js-yaml@^3.5.4, js-yaml@^3.7.0, argparse "^1.0.7" esprima "^4.0.0" -js-yaml@~3.4.0: - version "3.4.6" - resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.4.6.tgz#6be1b23f6249f53d293370fd4d1aaa63ce1b4eb0" - dependencies: - argparse "^1.0.2" - esprima "^2.6.0" - inherit "^2.2.2" - js-yaml@~3.5.2: version "3.5.5" resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.5.5.tgz#0377c38017cabc7322b0d1fbcd25a491641f2fbe" @@ -6814,54 +6716,6 @@ jscodeshift@^0.5.0: temp "^0.8.1" write-file-atomic "^1.2.0" -jscs-jsdoc@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/jscs-jsdoc/-/jscs-jsdoc-2.0.0.tgz#f53ebce029aa3125bd88290ba50d64d4510a4871" - dependencies: - comment-parser "^0.3.1" - jsdoctypeparser "~1.2.0" - -jscs-preset-wikimedia@~1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/jscs-preset-wikimedia/-/jscs-preset-wikimedia-1.0.1.tgz#a6a5fa5967fd67a5d609038e1c794eaf41d4233d" - -jscs@~3.0.5: - version "3.0.7" - resolved "https://registry.yarnpkg.com/jscs/-/jscs-3.0.7.tgz#7141b4dff5b86e32d0e99d764b836767c30d201a" - dependencies: - chalk "~1.1.0" - cli-table "~0.3.1" - commander "~2.9.0" - cst "^0.4.3" - estraverse "^4.1.0" - exit "~0.1.2" - glob "^5.0.1" - htmlparser2 "3.8.3" - js-yaml "~3.4.0" - jscs-jsdoc "^2.0.0" - jscs-preset-wikimedia "~1.0.0" - jsonlint "~1.6.2" - lodash "~3.10.0" - minimatch "~3.0.0" - natural-compare "~1.2.2" - pathval "~0.1.1" - prompt "~0.2.14" - reserved-words "^0.1.1" - resolve "^1.1.6" - strip-bom "^2.0.0" - strip-json-comments "~1.0.2" - to-double-quotes "^2.0.0" - to-single-quotes "^2.0.0" - vow "~0.4.8" - vow-fs "~0.3.4" - xmlbuilder "^3.1.0" - -jsdoctypeparser@~1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/jsdoctypeparser/-/jsdoctypeparser-1.2.0.tgz#e7dedc153a11849ffc5141144ae86a7ef0c25392" - dependencies: - lodash "^3.7.0" - jsdom@^11.5.1: version "11.11.0" resolved "https://registry.yarnpkg.com/jsdom/-/jsdom-11.11.0.tgz#df486efad41aee96c59ad7a190e2449c7eb1110e" @@ -6901,30 +6755,6 @@ jsesc@^1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-1.3.0.tgz#46c3fec8c1892b12b0833db9bc7622176dbab34b" -jshint-stylish@~2.2.1: - version "2.2.1" - resolved "https://registry.yarnpkg.com/jshint-stylish/-/jshint-stylish-2.2.1.tgz#242082a2c035ae03fd81044e0570cc4208cf6e61" - dependencies: - beeper "^1.1.0" - chalk "^1.0.0" - log-symbols "^1.0.0" - plur "^2.1.0" - string-length "^1.0.0" - text-table "^0.2.0" - -jshint@~2.9.4: - version "2.9.5" - resolved "https://registry.yarnpkg.com/jshint/-/jshint-2.9.5.tgz#1e7252915ce681b40827ee14248c46d34e9aa62c" - dependencies: - cli "~1.0.0" - console-browserify "1.1.x" - exit "0.1.x" - htmlparser2 "3.8.x" - lodash "3.7.x" - minimatch "~3.0.2" - shelljs "0.3.x" - strip-json-comments "1.0.x" - json-buffer@3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/json-buffer/-/json-buffer-3.0.0.tgz#5b1f397afc75d677bde8bcfc0e47e1f9a3d9a898" @@ -6981,13 +6811,6 @@ jsonify@~0.0.0: version "0.0.0" resolved "https://registry.yarnpkg.com/jsonify/-/jsonify-0.0.0.tgz#2c74b6ee41d93ca51b7b5aaee8f503631d252a73" -jsonlint@~1.6.2: - version "1.6.3" - resolved "https://registry.yarnpkg.com/jsonlint/-/jsonlint-1.6.3.tgz#cb5e31efc0b78291d0d862fbef05900adf212988" - dependencies: - JSV "^4.0.x" - nomnom "^1.5.x" - jsonparse@^1.2.0: version "1.3.1" resolved "https://registry.yarnpkg.com/jsonparse/-/jsonparse-1.3.1.tgz#3f4dae4a91fac315f71062f8521cc239f1366280" @@ -7497,11 +7320,7 @@ lodash.without@~4.4.0: version "4.4.0" resolved "https://registry.yarnpkg.com/lodash.without/-/lodash.without-4.4.0.tgz#3cd4574a00b67bae373a94b748772640507b7aac" -lodash@3.7.x: - version "3.7.0" - resolved "https://registry.yarnpkg.com/lodash/-/lodash-3.7.0.tgz#3678bd8ab995057c07ade836ed2ef087da811d45" - -lodash@^3.10.1, lodash@^3.5.0, lodash@^3.6.0, lodash@^3.7.0, lodash@^3.8.0, lodash@~3.10.0: +lodash@^3.10.1, lodash@^3.6.0, lodash@^3.8.0: version "3.10.1" resolved "https://registry.yarnpkg.com/lodash/-/lodash-3.10.1.tgz#5bf45e8e49ba4189e17d482789dfd15bd140b7b6" @@ -7513,11 +7332,7 @@ lodash@~4.3.0: version "4.3.0" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.3.0.tgz#efd9c4a6ec53f3b05412429915c3e4824e4d25a4" -lodash@~4.6.1: - version "4.6.1" - resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.6.1.tgz#df00c1164ad236b183cfc3887a5e8d38cc63cbbc" - -log-symbols@^1.0.0, log-symbols@^1.0.2: +log-symbols@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-1.0.2.tgz#376ff7b58ea3086a0f09facc74617eca501e1a18" dependencies: @@ -7990,7 +7805,7 @@ mixin-object@^2.0.1: for-in "^0.1.3" is-extendable "^0.1.1" -mkdirp@0.5.1, mkdirp@0.5.x, mkdirp@0.x.x, "mkdirp@>=0.5 0", mkdirp@^0.5.0, mkdirp@^0.5.1, mkdirp@~0.5.0, mkdirp@~0.5.1: +mkdirp@0.5.1, mkdirp@0.5.x, "mkdirp@>=0.5 0", mkdirp@^0.5.0, mkdirp@^0.5.1, mkdirp@~0.5.0, mkdirp@~0.5.1: version "0.5.1" resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903" dependencies: @@ -8121,20 +7936,12 @@ natural-compare@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" -natural-compare@~1.2.2: - version "1.2.2" - resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.2.2.tgz#1f96d60e3141cac1b6d05653ce0daeac763af6aa" - ncname@1.0.x: version "1.0.0" resolved "https://registry.yarnpkg.com/ncname/-/ncname-1.0.0.tgz#5b57ad18b1ca092864ef62b0b1ed8194f383b71c" dependencies: xml-char-classes "^1.0.0" -ncp@0.4.x: - version "0.4.2" - resolved "https://registry.yarnpkg.com/ncp/-/ncp-0.4.2.tgz#abcc6cbd3ec2ed2a729ff6e7c1fa8f01784a8574" - nearley@^2.7.10: version "2.13.0" resolved "https://registry.yarnpkg.com/nearley/-/nearley-2.13.0.tgz#6e7b0f4e68bfc3e74c99eaef2eda39e513143439" @@ -8359,7 +8166,7 @@ node-sass@^4.7.2: stdout-stream "^1.4.0" "true-case-path" "^1.0.2" -nomnom@^1.5.x, nomnom@^1.8.1: +nomnom@^1.8.1: version "1.8.1" resolved "https://registry.yarnpkg.com/nomnom/-/nomnom-1.8.1.tgz#2151f722472ba79e50a76fc125bb8c8f2e4dc2a7" dependencies: @@ -9207,10 +9014,6 @@ path-type@^3.0.0: dependencies: pify "^3.0.0" -pathval@~0.1.1: - version "0.1.1" - resolved "https://registry.yarnpkg.com/pathval/-/pathval-0.1.1.tgz#08f911cdca9cce5942880da7817bc0b723b66d82" - pbkdf2@^3.0.3: version "3.0.16" resolved "https://registry.yarnpkg.com/pbkdf2/-/pbkdf2-3.0.16.tgz#7404208ec6b01b62d85bf83853a8064f8d9c2a5c" @@ -9273,20 +9076,6 @@ pkg-up@^1.0.0: dependencies: find-up "^1.0.0" -pkginfo@0.3.x: - version "0.3.1" - resolved "https://registry.yarnpkg.com/pkginfo/-/pkginfo-0.3.1.tgz#5b29f6a81f70717142e09e765bbeab97b4f81e21" - -pkginfo@0.x.x: - version "0.4.1" - resolved "https://registry.yarnpkg.com/pkginfo/-/pkginfo-0.4.1.tgz#b5418ef0439de5425fc4995042dced14fb2a84ff" - -plur@^2.1.0: - version "2.1.2" - resolved "https://registry.yarnpkg.com/plur/-/plur-2.1.2.tgz#7482452c1a0f508e3e344eaec312c91c29dc655a" - dependencies: - irregular-plurals "^1.0.0" - pluralize@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/pluralize/-/pluralize-1.2.1.tgz#d1a21483fd22bb41e58a12fa3421823140897c45" @@ -9810,16 +9599,6 @@ promise@^7.1.1: dependencies: asap "~2.0.3" -prompt@~0.2.14: - version "0.2.14" - resolved "https://registry.yarnpkg.com/prompt/-/prompt-0.2.14.tgz#57754f64f543fd7b0845707c818ece618f05ffdc" - dependencies: - pkginfo "0.x.x" - read "1.0.x" - revalidator "0.1.x" - utile "0.2.x" - winston "0.8.x" - promzard@^0.3.0: version "0.3.0" resolved "https://registry.yarnpkg.com/promzard/-/promzard-0.3.0.tgz#26a5d6ee8c7dee4cb12208305acfb93ba382a9ee" @@ -10304,7 +10083,7 @@ read-pkg@^3.0.0: normalize-package-data "^2.3.2" path-type "^3.0.0" -read@1, read@1.0.x, read@~1.0.1, read@~1.0.7: +read@1, read@~1.0.1, read@~1.0.7: version "1.0.7" resolved "https://registry.yarnpkg.com/read/-/read-1.0.7.tgz#b3da19bd052431a97671d44a42634adf710b40c4" dependencies: @@ -10331,15 +10110,6 @@ readable-stream@1.0, readable-stream@~1.0.2: isarray "0.0.1" string_decoder "~0.10.x" -readable-stream@1.1: - version "1.1.13" - resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.1.13.tgz#f6eef764f514c89e2b9e23146a75ba106756d23e" - dependencies: - core-util-is "~1.0.0" - inherits "~2.0.1" - isarray "0.0.1" - string_decoder "~0.10.x" - readable-stream@~1.1.10: version "1.1.14" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.1.14.tgz#7cf4c54ef648e3813084c636dd2079e166c081d9" @@ -10660,10 +10430,6 @@ requires-port@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff" -reserved-words@^0.1.1: - version "0.1.2" - resolved "https://registry.yarnpkg.com/reserved-words/-/reserved-words-0.1.2.tgz#00a0940f98cd501aeaaac316411d9adc52b31ab1" - resolve-cwd@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/resolve-cwd/-/resolve-cwd-2.0.0.tgz#00a9f7387556e27038eae232caa372a6a59b665a" @@ -10754,17 +10520,13 @@ retry@^0.12.0: version "0.12.0" resolved "https://registry.yarnpkg.com/retry/-/retry-0.12.0.tgz#1b42a6266a21f07421d1b0b54b7dc167b01c013b" -revalidator@0.1.x: - version "0.1.8" - resolved "https://registry.yarnpkg.com/revalidator/-/revalidator-0.1.8.tgz#fece61bfa0c1b52a206bd6b18198184bdd523a3b" - right-align@^0.1.1: version "0.1.3" resolved "https://registry.yarnpkg.com/right-align/-/right-align-0.1.3.tgz#61339b722fe6a3515689210d24e14c96148613ef" dependencies: align-text "^0.1.1" -rimraf@2, rimraf@2.x.x, rimraf@^2.2.8, rimraf@^2.4.4, rimraf@^2.5.1, rimraf@^2.5.2, rimraf@^2.5.4, rimraf@^2.6.0, rimraf@^2.6.1, rimraf@^2.6.2, rimraf@~2.6.2: +rimraf@2, rimraf@^2.2.8, rimraf@^2.4.4, rimraf@^2.5.1, rimraf@^2.5.2, rimraf@^2.5.4, rimraf@^2.6.0, rimraf@^2.6.1, rimraf@^2.6.2, rimraf@~2.6.2: version "2.6.2" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.2.tgz#2ed8150d24a16ea8651e6d6ef0f47c4158ce7a36" dependencies: @@ -11116,10 +10878,6 @@ shell-quote@^1.6.1: array-reduce "~0.0.0" jsonify "~0.0.0" -shelljs@0.3.x: - version "0.3.0" - resolved "https://registry.yarnpkg.com/shelljs/-/shelljs-0.3.0.tgz#3596e6307a781544f591f37da618360f31db57b1" - shelljs@^0.6.0: version "0.6.1" resolved "https://registry.yarnpkg.com/shelljs/-/shelljs-0.6.1.tgz#ec6211bed1920442088fe0f70b2837232ed2c8a8" @@ -11432,7 +11190,7 @@ source-map-resolve@^0.5.0: source-map-url "^0.4.0" urix "^0.1.0" -source-map-support@^0.4.0, source-map-support@^0.4.15: +source-map-support@^0.4.15: version "0.4.18" resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.4.18.tgz#0286a6de8be42641338594e97ccea75f0a2c585f" dependencies: @@ -11555,10 +11313,6 @@ stack-parser@^0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/stack-parser/-/stack-parser-0.0.1.tgz#7d3b63a17887e9e2c2bf55dbd3318fe34a39d1e7" -stack-trace@0.0.x: - version "0.0.10" - resolved "https://registry.yarnpkg.com/stack-trace/-/stack-trace-0.0.10.tgz#547c70b347e8d32b4e108ea1a2a159e5fdde19c0" - stack-utils@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/stack-utils/-/stack-utils-1.0.1.tgz#d4f33ab54e8e38778b0ca5cfd3b3afb12db68620" @@ -11649,12 +11403,6 @@ strict-uri-encode@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/strict-uri-encode/-/strict-uri-encode-2.0.0.tgz#b9c7330c7042862f6b142dc274bbcc5866ce3546" -string-length@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/string-length/-/string-length-1.0.1.tgz#56970fb1c38558e9e70b728bf3de269ac45adfac" - dependencies: - strip-ansi "^3.0.0" - string-length@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/string-length/-/string-length-2.0.0.tgz#d40dbb686a3ace960c1cffca562bf2c45f8363ed" @@ -11766,7 +11514,7 @@ strip-indent@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/strip-indent/-/strip-indent-2.0.0.tgz#5ef8db295d01e6ed6cbf7aab96998d7822527b68" -strip-json-comments@1.0.x, strip-json-comments@~1.0.1, strip-json-comments@~1.0.2: +strip-json-comments@~1.0.1: version "1.0.4" resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-1.0.4.tgz#1e15fbcac97d3ee99bf2d73b4c656b082bbafb91" @@ -12029,10 +11777,6 @@ to-buffer@^1.1.0: version "1.1.1" resolved "https://registry.yarnpkg.com/to-buffer/-/to-buffer-1.1.1.tgz#493bd48f62d7c43fcded313a03dcadb2e1213a80" -to-double-quotes@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/to-double-quotes/-/to-double-quotes-2.0.0.tgz#aaf231d6fa948949f819301bbab4484d8588e4a7" - to-fast-properties@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-1.0.3.tgz#b83571fa4d8c25b82e231b06e3a3055de4ca1a47" @@ -12059,10 +11803,6 @@ to-regex@^3.0.1, to-regex@^3.0.2: regex-not "^1.0.2" safe-regex "^1.1.0" -to-single-quotes@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/to-single-quotes/-/to-single-quotes-2.0.1.tgz#7cc29151f0f5f2c41946f119f5932fe554170125" - toposort@^1.0.0: version "1.0.7" resolved "https://registry.yarnpkg.com/toposort/-/toposort-1.0.7.tgz#2e68442d9f64ec720b8cc89e6443ac6caa950029" @@ -12527,25 +12267,10 @@ utila@~0.4: version "0.4.0" resolved "https://registry.yarnpkg.com/utila/-/utila-0.4.0.tgz#8a16a05d445657a3aea5eecc5b12a4fa5379772c" -utile@0.2.x: - version "0.2.1" - resolved "https://registry.yarnpkg.com/utile/-/utile-0.2.1.tgz#930c88e99098d6220834c356cbd9a770522d90d7" - dependencies: - async "~0.2.9" - deep-equal "*" - i "0.3.x" - mkdirp "0.x.x" - ncp "0.4.x" - rimraf "2.x.x" - utils-merge@1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" -uuid@^2.0.2: - version "2.0.3" - resolved "https://registry.yarnpkg.com/uuid/-/uuid-2.0.3.tgz#67e2e863797215530dff318e5bf9dcebfd47b21a" - uuid@^3.0.0, uuid@^3.0.1, uuid@^3.1.0, uuid@^3.2.1: version "3.2.1" resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.2.1.tgz#12c528bb9d58d0b9265d9a2f6f0fe8be17ff1f14" @@ -12623,25 +12348,6 @@ void-elements@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/void-elements/-/void-elements-2.0.1.tgz#c066afb582bb1cb4128d60ea92392e94d5e9dbec" -vow-fs@~0.3.4: - version "0.3.6" - resolved "https://registry.yarnpkg.com/vow-fs/-/vow-fs-0.3.6.tgz#2d4c59be22e2bf2618ddf597ab4baa923be7200d" - dependencies: - glob "^7.0.5" - uuid "^2.0.2" - vow "^0.4.7" - vow-queue "^0.4.1" - -vow-queue@^0.4.1: - version "0.4.3" - resolved "https://registry.yarnpkg.com/vow-queue/-/vow-queue-0.4.3.tgz#4ba8f64b56e9212c0dbe57f1405aeebd54cce78d" - dependencies: - vow "^0.4.17" - -vow@^0.4.17, vow@^0.4.7, vow@~0.4.1, vow@~0.4.8: - version "0.4.17" - resolved "https://registry.yarnpkg.com/vow/-/vow-0.4.17.tgz#b16e08fae58c52f3ebc6875f2441b26a92682904" - vue-parser@^1.1.5: version "1.1.6" resolved "https://registry.yarnpkg.com/vue-parser/-/vue-parser-1.1.6.tgz#3063c8431795664ebe429c23b5506899706e6355" @@ -12960,18 +12666,6 @@ window-size@0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/window-size/-/window-size-0.1.0.tgz#5438cd2ea93b202efa3a19fe8887aee7c94f9c9d" -winston@0.8.x: - version "0.8.3" - resolved "https://registry.yarnpkg.com/winston/-/winston-0.8.3.tgz#64b6abf4cd01adcaefd5009393b1d8e8bec19db0" - dependencies: - async "0.2.x" - colors "0.6.x" - cycle "1.0.x" - eyes "0.1.x" - isstream "0.1.x" - pkginfo "0.3.x" - stack-trace "0.0.x" - wordwrap@0.0.2: version "0.0.2" resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-0.0.2.tgz#b79669bb42ecb409f83d583cad52ca17eaa1643f" @@ -13053,12 +12747,6 @@ xml-name-validator@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/xml-name-validator/-/xml-name-validator-3.0.0.tgz#6ae73e06de4d8c6e47f9fb181f78d648ad457c6a" -xmlbuilder@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-3.1.0.tgz#2c86888f2d4eade850fa38ca7f7223f7209516e1" - dependencies: - lodash "^3.5.0" - xmlhttprequest-ssl@1.5.3: version "1.5.3" resolved "https://registry.yarnpkg.com/xmlhttprequest-ssl/-/xmlhttprequest-ssl-1.5.3.tgz#185a888c04eca46c3e4070d99f7b49de3528992d" From 739bee020779fa9af6ac88f087b33cc1d37328df Mon Sep 17 00:00:00 2001 From: Tobias Skarhed Date: Mon, 13 Aug 2018 16:08:01 +0200 Subject: [PATCH 398/786] Karma to Jest: graph (refactor) (#12860) * Begin conversion * Test setup started * Begin rewrite of graph * Rewrite as class * Some tests passing * Fix binding errors * Half tests passing * Call buildFlotPairs. More tests passing * All tests passing * Remove test test * Remove Karma test * Make methods out of event functions * Rename GraphElement --- public/app/plugins/panel/graph/graph.ts | 1403 +++++++++-------- public/app/plugins/panel/graph/module.ts | 1 + .../plugins/panel/graph/specs/graph.jest.ts | 518 ++++++ .../plugins/panel/graph/specs/graph_specs.ts | 454 ------ 4 files changed, 1236 insertions(+), 1140 deletions(-) create mode 100644 public/app/plugins/panel/graph/specs/graph.jest.ts delete mode 100644 public/app/plugins/panel/graph/specs/graph_specs.ts diff --git a/public/app/plugins/panel/graph/graph.ts b/public/app/plugins/panel/graph/graph.ts index 9f216c12288..35886aa5bf7 100755 --- a/public/app/plugins/panel/graph/graph.ts +++ b/public/app/plugins/panel/graph/graph.ts @@ -21,699 +21,730 @@ import { convertToHistogramData } from './histogram'; import { alignYLevel } from './align_yaxes'; import config from 'app/core/config'; +import { GraphCtrl } from './module'; + +class GraphElement { + ctrl: GraphCtrl; + tooltip: any; + dashboard: any; + annotations: Array; + panel: any; + plot: any; + sortedSeries: Array; + data: Array; + panelWidth: number; + eventManager: EventManager; + thresholdManager: ThresholdManager; + + constructor(private scope, private elem, private timeSrv) { + this.ctrl = scope.ctrl; + this.dashboard = this.ctrl.dashboard; + this.panel = this.ctrl.panel; + this.annotations = []; + + this.panelWidth = 0; + this.eventManager = new EventManager(this.ctrl); + this.thresholdManager = new ThresholdManager(this.ctrl); + this.tooltip = new GraphTooltip(this.elem, this.ctrl.dashboard, this.scope, () => { + return this.sortedSeries; + }); + + // panel events + this.ctrl.events.on('panel-teardown', this.onPanelteardown.bind(this)); + + /** + * Split graph rendering into two parts. + * First, calculate series stats in buildFlotPairs() function. Then legend rendering started + * (see ctrl.events.on('render') in legend.ts). + * When legend is rendered it emits 'legend-rendering-complete' and graph rendered. + */ + this.ctrl.events.on('render', this.onRender.bind(this)); + this.ctrl.events.on('legend-rendering-complete', this.onLegendRenderingComplete.bind(this)); + + // global events + appEvents.on('graph-hover', this.onGraphHover.bind(this), scope); + + appEvents.on('graph-hover-clear', this.onGraphHoverClear.bind(this), scope); + + this.elem.bind('plotselected', this.onPlotSelected.bind(this)); + + this.elem.bind('plotclick', this.onPlotClick.bind(this)); + scope.$on('$destroy', this.onScopeDestroy.bind(this)); + } + + onRender(renderData) { + this.data = renderData || this.data; + if (!this.data) { + return; + } + this.annotations = this.ctrl.annotations || []; + this.buildFlotPairs(this.data); + const graphHeight = this.elem.height(); + updateLegendValues(this.data, this.panel, graphHeight); + + this.ctrl.events.emit('render-legend'); + } + + onGraphHover(evt) { + // ignore other graph hover events if shared tooltip is disabled + if (!this.dashboard.sharedTooltipModeEnabled()) { + return; + } + + // ignore if we are the emitter + if (!this.plot || evt.panel.id === this.panel.id || this.ctrl.otherPanelInFullscreenMode()) { + return; + } + + this.tooltip.show(evt.pos); + } + + onPanelteardown() { + this.thresholdManager = null; + + if (this.plot) { + this.plot.destroy(); + this.plot = null; + } + } + + onLegendRenderingComplete() { + this.render_panel(); + } + + onGraphHoverClear(event, info) { + if (this.plot) { + this.tooltip.clear(this.plot); + } + } + + onPlotSelected(event, ranges) { + if (this.panel.xaxis.mode !== 'time') { + // Skip if panel in histogram or series mode + this.plot.clearSelection(); + return; + } + + if ((ranges.ctrlKey || ranges.metaKey) && (this.dashboard.meta.canEdit || this.dashboard.meta.canMakeEditable)) { + // Add annotation + setTimeout(() => { + this.eventManager.updateTime(ranges.xaxis); + }, 100); + } else { + this.scope.$apply(() => { + this.timeSrv.setTime({ + from: moment.utc(ranges.xaxis.from), + to: moment.utc(ranges.xaxis.to), + }); + }); + } + } + + onPlotClick(event, pos, item) { + if (this.panel.xaxis.mode !== 'time') { + // Skip if panel in histogram or series mode + return; + } + + if ((pos.ctrlKey || pos.metaKey) && (this.dashboard.meta.canEdit || this.dashboard.meta.canMakeEditable)) { + // Skip if range selected (added in "plotselected" event handler) + let isRangeSelection = pos.x !== pos.x1; + if (!isRangeSelection) { + setTimeout(() => { + this.eventManager.updateTime({ from: pos.x, to: null }); + }, 100); + } + } + } + + onScopeDestroy() { + this.tooltip.destroy(); + this.elem.off(); + this.elem.remove(); + } + + shouldAbortRender() { + if (!this.data) { + return true; + } + + if (this.panelWidth === 0) { + return true; + } + + return false; + } + + drawHook(plot) { + // add left axis labels + if (this.panel.yaxes[0].label && this.panel.yaxes[0].show) { + $("
    ") + .text(this.panel.yaxes[0].label) + .appendTo(this.elem); + } + + // add right axis labels + if (this.panel.yaxes[1].label && this.panel.yaxes[1].show) { + $("
    ") + .text(this.panel.yaxes[1].label) + .appendTo(this.elem); + } + + if (this.ctrl.dataWarning) { + $(`
    ${this.ctrl.dataWarning.title}
    `).appendTo(this.elem); + } + + this.thresholdManager.draw(plot); + } + + processOffsetHook(plot, gridMargin) { + var left = this.panel.yaxes[0]; + var right = this.panel.yaxes[1]; + if (left.show && left.label) { + gridMargin.left = 20; + } + if (right.show && right.label) { + gridMargin.right = 20; + } + + // apply y-axis min/max options + var yaxis = plot.getYAxes(); + for (var i = 0; i < yaxis.length; i++) { + var axis = yaxis[i]; + var panelOptions = this.panel.yaxes[i]; + axis.options.max = axis.options.max !== null ? axis.options.max : panelOptions.max; + axis.options.min = axis.options.min !== null ? axis.options.min : panelOptions.min; + } + } + + processRangeHook(plot) { + var yAxes = plot.getYAxes(); + const align = this.panel.yaxis.align || false; + + if (yAxes.length > 1 && align === true) { + const level = this.panel.yaxis.alignLevel || 0; + alignYLevel(yAxes, parseFloat(level)); + } + } + + // Series could have different timeSteps, + // let's find the smallest one so that bars are correctly rendered. + // In addition, only take series which are rendered as bars for this. + getMinTimeStepOfSeries(data) { + var min = Number.MAX_VALUE; + + for (let i = 0; i < data.length; i++) { + if (!data[i].stats.timeStep) { + continue; + } + if (this.panel.bars) { + if (data[i].bars && data[i].bars.show === false) { + continue; + } + } else { + if (typeof data[i].bars === 'undefined' || typeof data[i].bars.show === 'undefined' || !data[i].bars.show) { + continue; + } + } + + if (data[i].stats.timeStep < min) { + min = data[i].stats.timeStep; + } + } + + return min; + } + + // Function for rendering panel + render_panel() { + this.panelWidth = this.elem.width(); + if (this.shouldAbortRender()) { + return; + } + + // give space to alert editing + this.thresholdManager.prepare(this.elem, this.data); + + // un-check dashes if lines are unchecked + this.panel.dashes = this.panel.lines ? this.panel.dashes : false; + + // Populate element + let options: any = this.buildFlotOptions(this.panel); + this.prepareXAxis(options, this.panel); + this.configureYAxisOptions(this.data, options); + this.thresholdManager.addFlotOptions(options, this.panel); + this.eventManager.addFlotEvents(this.annotations, options); + + this.sortedSeries = this.sortSeries(this.data, this.panel); + this.callPlot(options, true); + } + + buildFlotPairs(data) { + for (let i = 0; i < data.length; i++) { + let series = data[i]; + series.data = series.getFlotPairs(series.nullPointMode || this.panel.nullPointMode); + + // if hidden remove points and disable stack + if (this.ctrl.hiddenSeries[series.alias]) { + series.data = []; + series.stack = false; + } + } + } + + prepareXAxis(options, panel) { + switch (panel.xaxis.mode) { + case 'series': { + options.series.bars.barWidth = 0.7; + options.series.bars.align = 'center'; + + for (let i = 0; i < this.data.length; i++) { + let series = this.data[i]; + series.data = [[i + 1, series.stats[panel.xaxis.values[0]]]]; + } + + this.addXSeriesAxis(options); + break; + } + case 'histogram': { + let bucketSize: number; + + if (this.data.length) { + let histMin = _.min(_.map(this.data, s => s.stats.min)); + let histMax = _.max(_.map(this.data, s => s.stats.max)); + let ticks = panel.xaxis.buckets || this.panelWidth / 50; + bucketSize = tickStep(histMin, histMax, ticks); + options.series.bars.barWidth = bucketSize * 0.8; + this.data = convertToHistogramData(this.data, bucketSize, this.ctrl.hiddenSeries, histMin, histMax); + } else { + bucketSize = 0; + } + + this.addXHistogramAxis(options, bucketSize); + break; + } + case 'table': { + options.series.bars.barWidth = 0.7; + options.series.bars.align = 'center'; + this.addXTableAxis(options); + break; + } + default: { + options.series.bars.barWidth = this.getMinTimeStepOfSeries(this.data) / 1.5; + this.addTimeAxis(options); + break; + } + } + } + + callPlot(options, incrementRenderCounter) { + try { + this.plot = $.plot(this.elem, this.sortedSeries, options); + if (this.ctrl.renderError) { + delete this.ctrl.error; + delete this.ctrl.inspector; + } + } catch (e) { + console.log('flotcharts error', e); + this.ctrl.error = e.message || 'Render Error'; + this.ctrl.renderError = true; + this.ctrl.inspector = { error: e }; + } + + if (incrementRenderCounter) { + this.ctrl.renderingCompleted(); + } + } + + buildFlotOptions(panel) { + let gridColor = '#c8c8c8'; + if (config.bootData.user.lightTheme === true) { + gridColor = '#a1a1a1'; + } + const stack = panel.stack ? true : null; + let options = { + hooks: { + draw: [this.drawHook.bind(this)], + processOffset: [this.processOffsetHook.bind(this)], + processRange: [this.processRangeHook.bind(this)], + }, + legend: { show: false }, + series: { + stackpercent: panel.stack ? panel.percentage : false, + stack: panel.percentage ? null : stack, + lines: { + show: panel.lines, + zero: false, + fill: this.translateFillOption(panel.fill), + lineWidth: panel.dashes ? 0 : panel.linewidth, + steps: panel.steppedLine, + }, + dashes: { + show: panel.dashes, + lineWidth: panel.linewidth, + dashLength: [panel.dashLength, panel.spaceLength], + }, + bars: { + show: panel.bars, + fill: 1, + barWidth: 1, + zero: false, + lineWidth: 0, + }, + points: { + show: panel.points, + fill: 1, + fillColor: false, + radius: panel.points ? panel.pointradius : 2, + }, + shadowSize: 0, + }, + yaxes: [], + xaxis: {}, + grid: { + minBorderMargin: 0, + markings: [], + backgroundColor: null, + borderWidth: 0, + hoverable: true, + clickable: true, + color: gridColor, + margin: { left: 0, right: 0 }, + labelMarginX: 0, + }, + selection: { + mode: 'x', + color: '#666', + }, + crosshair: { + mode: 'x', + }, + }; + return options; + } + + sortSeries(series, panel) { + var sortBy = panel.legend.sort; + var sortOrder = panel.legend.sortDesc; + var haveSortBy = sortBy !== null && sortBy !== undefined; + var haveSortOrder = sortOrder !== null && sortOrder !== undefined; + var shouldSortBy = panel.stack && haveSortBy && haveSortOrder; + var sortDesc = panel.legend.sortDesc === true ? -1 : 1; + + if (shouldSortBy) { + return _.sortBy(series, s => s.stats[sortBy] * sortDesc); + } else { + return _.sortBy(series, s => s.zindex); + } + } + + translateFillOption(fill) { + if (this.panel.percentage && this.panel.stack) { + return fill === 0 ? 0.001 : fill / 10; + } else { + return fill / 10; + } + } + + addTimeAxis(options) { + var ticks = this.panelWidth / 100; + var min = _.isUndefined(this.ctrl.range.from) ? null : this.ctrl.range.from.valueOf(); + var max = _.isUndefined(this.ctrl.range.to) ? null : this.ctrl.range.to.valueOf(); + + options.xaxis = { + timezone: this.dashboard.getTimezone(), + show: this.panel.xaxis.show, + mode: 'time', + min: min, + max: max, + label: 'Datetime', + ticks: ticks, + timeformat: this.time_format(ticks, min, max), + }; + } + + addXSeriesAxis(options) { + var ticks = _.map(this.data, function(series, index) { + return [index + 1, series.alias]; + }); + + options.xaxis = { + timezone: this.dashboard.getTimezone(), + show: this.panel.xaxis.show, + mode: null, + min: 0, + max: ticks.length + 1, + label: 'Datetime', + ticks: ticks, + }; + } + + addXHistogramAxis(options, bucketSize) { + let ticks, min, max; + let defaultTicks = this.panelWidth / 50; + + if (this.data.length && bucketSize) { + let tick_values = []; + for (let d of this.data) { + for (let point of d.data) { + tick_values[point[0]] = true; + } + } + ticks = Object.keys(tick_values).map(v => Number(v)); + min = _.min(ticks); + max = _.max(ticks); + + // Adjust tick step + let tickStep = bucketSize; + let ticks_num = Math.floor((max - min) / tickStep); + while (ticks_num > defaultTicks) { + tickStep = tickStep * 2; + ticks_num = Math.ceil((max - min) / tickStep); + } + + // Expand ticks for pretty view + min = Math.floor(min / tickStep) * tickStep; + // 1.01 is 101% - ensure we have enough space for last bar + max = Math.ceil(max * 1.01 / tickStep) * tickStep; + + ticks = []; + for (let i = min; i <= max; i += tickStep) { + ticks.push(i); + } + } else { + // Set defaults if no data + ticks = defaultTicks / 2; + min = 0; + max = 1; + } + + options.xaxis = { + timezone: this.dashboard.getTimezone(), + show: this.panel.xaxis.show, + mode: null, + min: min, + max: max, + label: 'Histogram', + ticks: ticks, + }; + + // Use 'short' format for histogram values + this.configureAxisMode(options.xaxis, 'short'); + } + + addXTableAxis(options) { + var ticks = _.map(this.data, function(series, seriesIndex) { + return _.map(series.datapoints, function(point, pointIndex) { + var tickIndex = seriesIndex * series.datapoints.length + pointIndex; + return [tickIndex + 1, point[1]]; + }); + }); + ticks = _.flatten(ticks, true); + + options.xaxis = { + timezone: this.dashboard.getTimezone(), + show: this.panel.xaxis.show, + mode: null, + min: 0, + max: ticks.length + 1, + label: 'Datetime', + ticks: ticks, + }; + } + + configureYAxisOptions(data, options) { + var defaults = { + position: 'left', + show: this.panel.yaxes[0].show, + index: 1, + logBase: this.panel.yaxes[0].logBase || 1, + min: this.parseNumber(this.panel.yaxes[0].min), + max: this.parseNumber(this.panel.yaxes[0].max), + tickDecimals: this.panel.yaxes[0].decimals, + }; + + options.yaxes.push(defaults); + + if (_.find(data, { yaxis: 2 })) { + var secondY = _.clone(defaults); + secondY.index = 2; + secondY.show = this.panel.yaxes[1].show; + secondY.logBase = this.panel.yaxes[1].logBase || 1; + secondY.position = 'right'; + secondY.min = this.parseNumber(this.panel.yaxes[1].min); + secondY.max = this.parseNumber(this.panel.yaxes[1].max); + secondY.tickDecimals = this.panel.yaxes[1].decimals; + options.yaxes.push(secondY); + + this.applyLogScale(options.yaxes[1], data); + this.configureAxisMode( + options.yaxes[1], + this.panel.percentage && this.panel.stack ? 'percent' : this.panel.yaxes[1].format + ); + } + this.applyLogScale(options.yaxes[0], data); + this.configureAxisMode( + options.yaxes[0], + this.panel.percentage && this.panel.stack ? 'percent' : this.panel.yaxes[0].format + ); + } + + parseNumber(value: any) { + if (value === null || typeof value === 'undefined') { + return null; + } + + return _.toNumber(value); + } + + applyLogScale(axis, data) { + if (axis.logBase === 1) { + return; + } + + const minSetToZero = axis.min === 0; + + if (axis.min < Number.MIN_VALUE) { + axis.min = null; + } + if (axis.max < Number.MIN_VALUE) { + axis.max = null; + } + + var series, i; + var max = axis.max, + min = axis.min; + + for (i = 0; i < data.length; i++) { + series = data[i]; + if (series.yaxis === axis.index) { + if (!max || max < series.stats.max) { + max = series.stats.max; + } + if (!min || min > series.stats.logmin) { + min = series.stats.logmin; + } + } + } + + axis.transform = function(v) { + return v < Number.MIN_VALUE ? null : Math.log(v) / Math.log(axis.logBase); + }; + axis.inverseTransform = function(v) { + return Math.pow(axis.logBase, v); + }; + + if (!max && !min) { + max = axis.inverseTransform(+2); + min = axis.inverseTransform(-2); + } else if (!max) { + max = min * axis.inverseTransform(+4); + } else if (!min) { + min = max * axis.inverseTransform(-4); + } + + if (axis.min) { + min = axis.inverseTransform(Math.ceil(axis.transform(axis.min))); + } else { + min = axis.min = axis.inverseTransform(Math.floor(axis.transform(min))); + } + if (axis.max) { + max = axis.inverseTransform(Math.floor(axis.transform(axis.max))); + } else { + max = axis.max = axis.inverseTransform(Math.ceil(axis.transform(max))); + } + + if (!min || min < Number.MIN_VALUE || !max || max < Number.MIN_VALUE) { + return; + } + + if (Number.isFinite(min) && Number.isFinite(max)) { + if (minSetToZero) { + axis.min = 0.1; + min = 1; + } + + axis.ticks = this.generateTicksForLogScaleYAxis(min, max, axis.logBase); + if (minSetToZero) { + axis.ticks.unshift(0.1); + } + if (axis.ticks[axis.ticks.length - 1] > axis.max) { + axis.max = axis.ticks[axis.ticks.length - 1]; + } + } else { + axis.ticks = [1, 2]; + delete axis.min; + delete axis.max; + } + } + + generateTicksForLogScaleYAxis(min, max, logBase) { + let ticks = []; + + var nextTick; + for (nextTick = min; nextTick <= max; nextTick *= logBase) { + ticks.push(nextTick); + } + + const maxNumTicks = Math.ceil(this.ctrl.height / 25); + const numTicks = ticks.length; + if (numTicks > maxNumTicks) { + const factor = Math.ceil(numTicks / maxNumTicks) * logBase; + ticks = []; + + for (nextTick = min; nextTick <= max * factor; nextTick *= factor) { + ticks.push(nextTick); + } + } + + return ticks; + } + + configureAxisMode(axis, format) { + axis.tickFormatter = function(val, axis) { + if (!kbn.valueFormats[format]) { + throw new Error(`Unit '${format}' is not supported`); + } + return kbn.valueFormats[format](val, axis.tickDecimals, axis.scaledDecimals); + }; + } + + time_format(ticks, min, max) { + if (min && max && ticks) { + var range = max - min; + var secPerTick = range / ticks / 1000; + var oneDay = 86400000; + var oneYear = 31536000000; + + if (secPerTick <= 45) { + return '%H:%M:%S'; + } + if (secPerTick <= 7200 || range <= oneDay) { + return '%H:%M'; + } + if (secPerTick <= 80000) { + return '%m/%d %H:%M'; + } + if (secPerTick <= 2419200 || range <= oneYear) { + return '%m/%d'; + } + return '%Y-%m'; + } + + return '%H:%M'; + } +} + /** @ngInject **/ function graphDirective(timeSrv, popoverSrv, contextSrv) { return { restrict: 'A', template: '', - link: function(scope, elem) { - var ctrl = scope.ctrl; - var dashboard = ctrl.dashboard; - var panel = ctrl.panel; - var annotations = []; - var data; - var plot; - var sortedSeries; - var panelWidth = 0; - var eventManager = new EventManager(ctrl); - var thresholdManager = new ThresholdManager(ctrl); - var tooltip = new GraphTooltip(elem, dashboard, scope, function() { - return sortedSeries; - }); - - // panel events - ctrl.events.on('panel-teardown', () => { - thresholdManager = null; - - if (plot) { - plot.destroy(); - plot = null; - } - }); - - /** - * Split graph rendering into two parts. - * First, calculate series stats in buildFlotPairs() function. Then legend rendering started - * (see ctrl.events.on('render') in legend.ts). - * When legend is rendered it emits 'legend-rendering-complete' and graph rendered. - */ - ctrl.events.on('render', renderData => { - data = renderData || data; - if (!data) { - return; - } - annotations = ctrl.annotations || []; - buildFlotPairs(data); - const graphHeight = elem.height(); - updateLegendValues(data, panel, graphHeight); - - ctrl.events.emit('render-legend'); - }); - - ctrl.events.on('legend-rendering-complete', () => { - render_panel(); - }); - - // global events - appEvents.on( - 'graph-hover', - evt => { - // ignore other graph hover events if shared tooltip is disabled - if (!dashboard.sharedTooltipModeEnabled()) { - return; - } - - // ignore if we are the emitter - if (!plot || evt.panel.id === panel.id || ctrl.otherPanelInFullscreenMode()) { - return; - } - - tooltip.show(evt.pos); - }, - scope - ); - - appEvents.on( - 'graph-hover-clear', - (event, info) => { - if (plot) { - tooltip.clear(plot); - } - }, - scope - ); - - function shouldAbortRender() { - if (!data) { - return true; - } - - if (panelWidth === 0) { - return true; - } - - return false; - } - - function drawHook(plot) { - // add left axis labels - if (panel.yaxes[0].label && panel.yaxes[0].show) { - $("
    ") - .text(panel.yaxes[0].label) - .appendTo(elem); - } - - // add right axis labels - if (panel.yaxes[1].label && panel.yaxes[1].show) { - $("
    ") - .text(panel.yaxes[1].label) - .appendTo(elem); - } - - if (ctrl.dataWarning) { - $(`
    ${ctrl.dataWarning.title}
    `).appendTo(elem); - } - - thresholdManager.draw(plot); - } - - function processOffsetHook(plot, gridMargin) { - var left = panel.yaxes[0]; - var right = panel.yaxes[1]; - if (left.show && left.label) { - gridMargin.left = 20; - } - if (right.show && right.label) { - gridMargin.right = 20; - } - - // apply y-axis min/max options - var yaxis = plot.getYAxes(); - for (var i = 0; i < yaxis.length; i++) { - var axis = yaxis[i]; - var panelOptions = panel.yaxes[i]; - axis.options.max = axis.options.max !== null ? axis.options.max : panelOptions.max; - axis.options.min = axis.options.min !== null ? axis.options.min : panelOptions.min; - } - } - - function processRangeHook(plot) { - var yAxes = plot.getYAxes(); - const align = panel.yaxis.align || false; - - if (yAxes.length > 1 && align === true) { - const level = panel.yaxis.alignLevel || 0; - alignYLevel(yAxes, parseFloat(level)); - } - } - - // Series could have different timeSteps, - // let's find the smallest one so that bars are correctly rendered. - // In addition, only take series which are rendered as bars for this. - function getMinTimeStepOfSeries(data) { - var min = Number.MAX_VALUE; - - for (let i = 0; i < data.length; i++) { - if (!data[i].stats.timeStep) { - continue; - } - if (panel.bars) { - if (data[i].bars && data[i].bars.show === false) { - continue; - } - } else { - if (typeof data[i].bars === 'undefined' || typeof data[i].bars.show === 'undefined' || !data[i].bars.show) { - continue; - } - } - - if (data[i].stats.timeStep < min) { - min = data[i].stats.timeStep; - } - } - - return min; - } - - // Function for rendering panel - function render_panel() { - panelWidth = elem.width(); - if (shouldAbortRender()) { - return; - } - - // give space to alert editing - thresholdManager.prepare(elem, data); - - // un-check dashes if lines are unchecked - panel.dashes = panel.lines ? panel.dashes : false; - - // Populate element - let options: any = buildFlotOptions(panel); - prepareXAxis(options, panel); - configureYAxisOptions(data, options); - thresholdManager.addFlotOptions(options, panel); - eventManager.addFlotEvents(annotations, options); - - sortedSeries = sortSeries(data, panel); - callPlot(options, true); - } - - function buildFlotPairs(data) { - for (let i = 0; i < data.length; i++) { - let series = data[i]; - series.data = series.getFlotPairs(series.nullPointMode || panel.nullPointMode); - - // if hidden remove points and disable stack - if (ctrl.hiddenSeries[series.alias]) { - series.data = []; - series.stack = false; - } - } - } - - function prepareXAxis(options, panel) { - switch (panel.xaxis.mode) { - case 'series': { - options.series.bars.barWidth = 0.7; - options.series.bars.align = 'center'; - - for (let i = 0; i < data.length; i++) { - let series = data[i]; - series.data = [[i + 1, series.stats[panel.xaxis.values[0]]]]; - } - - addXSeriesAxis(options); - break; - } - case 'histogram': { - let bucketSize: number; - - if (data.length) { - let histMin = _.min(_.map(data, s => s.stats.min)); - let histMax = _.max(_.map(data, s => s.stats.max)); - let ticks = panel.xaxis.buckets || panelWidth / 50; - bucketSize = tickStep(histMin, histMax, ticks); - options.series.bars.barWidth = bucketSize * 0.8; - data = convertToHistogramData(data, bucketSize, ctrl.hiddenSeries, histMin, histMax); - } else { - bucketSize = 0; - } - - addXHistogramAxis(options, bucketSize); - break; - } - case 'table': { - options.series.bars.barWidth = 0.7; - options.series.bars.align = 'center'; - addXTableAxis(options); - break; - } - default: { - options.series.bars.barWidth = getMinTimeStepOfSeries(data) / 1.5; - addTimeAxis(options); - break; - } - } - } - - function callPlot(options, incrementRenderCounter) { - try { - plot = $.plot(elem, sortedSeries, options); - if (ctrl.renderError) { - delete ctrl.error; - delete ctrl.inspector; - } - } catch (e) { - console.log('flotcharts error', e); - ctrl.error = e.message || 'Render Error'; - ctrl.renderError = true; - ctrl.inspector = { error: e }; - } - - if (incrementRenderCounter) { - ctrl.renderingCompleted(); - } - } - - function buildFlotOptions(panel) { - let gridColor = '#c8c8c8'; - if (config.bootData.user.lightTheme === true) { - gridColor = '#a1a1a1'; - } - const stack = panel.stack ? true : null; - let options = { - hooks: { - draw: [drawHook], - processOffset: [processOffsetHook], - processRange: [processRangeHook], - }, - legend: { show: false }, - series: { - stackpercent: panel.stack ? panel.percentage : false, - stack: panel.percentage ? null : stack, - lines: { - show: panel.lines, - zero: false, - fill: translateFillOption(panel.fill), - lineWidth: panel.dashes ? 0 : panel.linewidth, - steps: panel.steppedLine, - }, - dashes: { - show: panel.dashes, - lineWidth: panel.linewidth, - dashLength: [panel.dashLength, panel.spaceLength], - }, - bars: { - show: panel.bars, - fill: 1, - barWidth: 1, - zero: false, - lineWidth: 0, - }, - points: { - show: panel.points, - fill: 1, - fillColor: false, - radius: panel.points ? panel.pointradius : 2, - }, - shadowSize: 0, - }, - yaxes: [], - xaxis: {}, - grid: { - minBorderMargin: 0, - markings: [], - backgroundColor: null, - borderWidth: 0, - hoverable: true, - clickable: true, - color: gridColor, - margin: { left: 0, right: 0 }, - labelMarginX: 0, - }, - selection: { - mode: 'x', - color: '#666', - }, - crosshair: { - mode: 'x', - }, - }; - return options; - } - - function sortSeries(series, panel) { - var sortBy = panel.legend.sort; - var sortOrder = panel.legend.sortDesc; - var haveSortBy = sortBy !== null && sortBy !== undefined; - var haveSortOrder = sortOrder !== null && sortOrder !== undefined; - var shouldSortBy = panel.stack && haveSortBy && haveSortOrder; - var sortDesc = panel.legend.sortDesc === true ? -1 : 1; - - if (shouldSortBy) { - return _.sortBy(series, s => s.stats[sortBy] * sortDesc); - } else { - return _.sortBy(series, s => s.zindex); - } - } - - function translateFillOption(fill) { - if (panel.percentage && panel.stack) { - return fill === 0 ? 0.001 : fill / 10; - } else { - return fill / 10; - } - } - - function addTimeAxis(options) { - var ticks = panelWidth / 100; - var min = _.isUndefined(ctrl.range.from) ? null : ctrl.range.from.valueOf(); - var max = _.isUndefined(ctrl.range.to) ? null : ctrl.range.to.valueOf(); - - options.xaxis = { - timezone: dashboard.getTimezone(), - show: panel.xaxis.show, - mode: 'time', - min: min, - max: max, - label: 'Datetime', - ticks: ticks, - timeformat: time_format(ticks, min, max), - }; - } - - function addXSeriesAxis(options) { - var ticks = _.map(data, function(series, index) { - return [index + 1, series.alias]; - }); - - options.xaxis = { - timezone: dashboard.getTimezone(), - show: panel.xaxis.show, - mode: null, - min: 0, - max: ticks.length + 1, - label: 'Datetime', - ticks: ticks, - }; - } - - function addXHistogramAxis(options, bucketSize) { - let ticks, min, max; - let defaultTicks = panelWidth / 50; - - if (data.length && bucketSize) { - let tick_values = []; - for (let d of data) { - for (let point of d.data) { - tick_values[point[0]] = true; - } - } - ticks = Object.keys(tick_values).map(v => Number(v)); - min = _.min(ticks); - max = _.max(ticks); - - // Adjust tick step - let tickStep = bucketSize; - let ticks_num = Math.floor((max - min) / tickStep); - while (ticks_num > defaultTicks) { - tickStep = tickStep * 2; - ticks_num = Math.ceil((max - min) / tickStep); - } - - // Expand ticks for pretty view - min = Math.floor(min / tickStep) * tickStep; - // 1.01 is 101% - ensure we have enough space for last bar - max = Math.ceil(max * 1.01 / tickStep) * tickStep; - - ticks = []; - for (let i = min; i <= max; i += tickStep) { - ticks.push(i); - } - } else { - // Set defaults if no data - ticks = defaultTicks / 2; - min = 0; - max = 1; - } - - options.xaxis = { - timezone: dashboard.getTimezone(), - show: panel.xaxis.show, - mode: null, - min: min, - max: max, - label: 'Histogram', - ticks: ticks, - }; - - // Use 'short' format for histogram values - configureAxisMode(options.xaxis, 'short'); - } - - function addXTableAxis(options) { - var ticks = _.map(data, function(series, seriesIndex) { - return _.map(series.datapoints, function(point, pointIndex) { - var tickIndex = seriesIndex * series.datapoints.length + pointIndex; - return [tickIndex + 1, point[1]]; - }); - }); - ticks = _.flatten(ticks, true); - - options.xaxis = { - timezone: dashboard.getTimezone(), - show: panel.xaxis.show, - mode: null, - min: 0, - max: ticks.length + 1, - label: 'Datetime', - ticks: ticks, - }; - } - - function configureYAxisOptions(data, options) { - var defaults = { - position: 'left', - show: panel.yaxes[0].show, - index: 1, - logBase: panel.yaxes[0].logBase || 1, - min: parseNumber(panel.yaxes[0].min), - max: parseNumber(panel.yaxes[0].max), - tickDecimals: panel.yaxes[0].decimals, - }; - - options.yaxes.push(defaults); - - if (_.find(data, { yaxis: 2 })) { - var secondY = _.clone(defaults); - secondY.index = 2; - secondY.show = panel.yaxes[1].show; - secondY.logBase = panel.yaxes[1].logBase || 1; - secondY.position = 'right'; - secondY.min = parseNumber(panel.yaxes[1].min); - secondY.max = parseNumber(panel.yaxes[1].max); - secondY.tickDecimals = panel.yaxes[1].decimals; - options.yaxes.push(secondY); - - applyLogScale(options.yaxes[1], data); - configureAxisMode(options.yaxes[1], panel.percentage && panel.stack ? 'percent' : panel.yaxes[1].format); - } - applyLogScale(options.yaxes[0], data); - configureAxisMode(options.yaxes[0], panel.percentage && panel.stack ? 'percent' : panel.yaxes[0].format); - } - - function parseNumber(value: any) { - if (value === null || typeof value === 'undefined') { - return null; - } - - return _.toNumber(value); - } - - function applyLogScale(axis, data) { - if (axis.logBase === 1) { - return; - } - - const minSetToZero = axis.min === 0; - - if (axis.min < Number.MIN_VALUE) { - axis.min = null; - } - if (axis.max < Number.MIN_VALUE) { - axis.max = null; - } - - var series, i; - var max = axis.max, - min = axis.min; - - for (i = 0; i < data.length; i++) { - series = data[i]; - if (series.yaxis === axis.index) { - if (!max || max < series.stats.max) { - max = series.stats.max; - } - if (!min || min > series.stats.logmin) { - min = series.stats.logmin; - } - } - } - - axis.transform = function(v) { - return v < Number.MIN_VALUE ? null : Math.log(v) / Math.log(axis.logBase); - }; - axis.inverseTransform = function(v) { - return Math.pow(axis.logBase, v); - }; - - if (!max && !min) { - max = axis.inverseTransform(+2); - min = axis.inverseTransform(-2); - } else if (!max) { - max = min * axis.inverseTransform(+4); - } else if (!min) { - min = max * axis.inverseTransform(-4); - } - - if (axis.min) { - min = axis.inverseTransform(Math.ceil(axis.transform(axis.min))); - } else { - min = axis.min = axis.inverseTransform(Math.floor(axis.transform(min))); - } - if (axis.max) { - max = axis.inverseTransform(Math.floor(axis.transform(axis.max))); - } else { - max = axis.max = axis.inverseTransform(Math.ceil(axis.transform(max))); - } - - if (!min || min < Number.MIN_VALUE || !max || max < Number.MIN_VALUE) { - return; - } - - if (Number.isFinite(min) && Number.isFinite(max)) { - if (minSetToZero) { - axis.min = 0.1; - min = 1; - } - - axis.ticks = generateTicksForLogScaleYAxis(min, max, axis.logBase); - if (minSetToZero) { - axis.ticks.unshift(0.1); - } - if (axis.ticks[axis.ticks.length - 1] > axis.max) { - axis.max = axis.ticks[axis.ticks.length - 1]; - } - } else { - axis.ticks = [1, 2]; - delete axis.min; - delete axis.max; - } - } - - function generateTicksForLogScaleYAxis(min, max, logBase) { - let ticks = []; - - var nextTick; - for (nextTick = min; nextTick <= max; nextTick *= logBase) { - ticks.push(nextTick); - } - - const maxNumTicks = Math.ceil(ctrl.height / 25); - const numTicks = ticks.length; - if (numTicks > maxNumTicks) { - const factor = Math.ceil(numTicks / maxNumTicks) * logBase; - ticks = []; - - for (nextTick = min; nextTick <= max * factor; nextTick *= factor) { - ticks.push(nextTick); - } - } - - return ticks; - } - - function configureAxisMode(axis, format) { - axis.tickFormatter = function(val, axis) { - if (!kbn.valueFormats[format]) { - throw new Error(`Unit '${format}' is not supported`); - } - return kbn.valueFormats[format](val, axis.tickDecimals, axis.scaledDecimals); - }; - } - - function time_format(ticks, min, max) { - if (min && max && ticks) { - var range = max - min; - var secPerTick = range / ticks / 1000; - var oneDay = 86400000; - var oneYear = 31536000000; - - if (secPerTick <= 45) { - return '%H:%M:%S'; - } - if (secPerTick <= 7200 || range <= oneDay) { - return '%H:%M'; - } - if (secPerTick <= 80000) { - return '%m/%d %H:%M'; - } - if (secPerTick <= 2419200 || range <= oneYear) { - return '%m/%d'; - } - return '%Y-%m'; - } - - return '%H:%M'; - } - - elem.bind('plotselected', function(event, ranges) { - if (panel.xaxis.mode !== 'time') { - // Skip if panel in histogram or series mode - plot.clearSelection(); - return; - } - - if ((ranges.ctrlKey || ranges.metaKey) && (dashboard.meta.canEdit || dashboard.meta.canMakeEditable)) { - // Add annotation - setTimeout(() => { - eventManager.updateTime(ranges.xaxis); - }, 100); - } else { - scope.$apply(function() { - timeSrv.setTime({ - from: moment.utc(ranges.xaxis.from), - to: moment.utc(ranges.xaxis.to), - }); - }); - } - }); - - elem.bind('plotclick', function(event, pos, item) { - if (panel.xaxis.mode !== 'time') { - // Skip if panel in histogram or series mode - return; - } - - if ((pos.ctrlKey || pos.metaKey) && (dashboard.meta.canEdit || dashboard.meta.canMakeEditable)) { - // Skip if range selected (added in "plotselected" event handler) - let isRangeSelection = pos.x !== pos.x1; - if (!isRangeSelection) { - setTimeout(() => { - eventManager.updateTime({ from: pos.x, to: null }); - }, 100); - } - } - }); - - scope.$on('$destroy', function() { - tooltip.destroy(); - elem.off(); - elem.remove(); - }); + link: (scope, elem) => { + return new GraphElement(scope, elem, timeSrv); }, }; } coreModule.directive('grafanaGraph', graphDirective); +export { GraphElement, graphDirective }; diff --git a/public/app/plugins/panel/graph/module.ts b/public/app/plugins/panel/graph/module.ts index ef82fb395a5..ba151692147 100644 --- a/public/app/plugins/panel/graph/module.ts +++ b/public/app/plugins/panel/graph/module.ts @@ -13,6 +13,7 @@ import { axesEditorComponent } from './axes_editor'; class GraphCtrl extends MetricsPanelCtrl { static template = template; + renderError: boolean; hiddenSeries: any = {}; seriesList: any = []; dataList: any = []; diff --git a/public/app/plugins/panel/graph/specs/graph.jest.ts b/public/app/plugins/panel/graph/specs/graph.jest.ts new file mode 100644 index 00000000000..f75f7cd68ea --- /dev/null +++ b/public/app/plugins/panel/graph/specs/graph.jest.ts @@ -0,0 +1,518 @@ +jest.mock('app/features/annotations/all', () => ({ + EventManager: function() { + return { + on: () => {}, + addFlotEvents: () => {}, + }; + }, +})); + +jest.mock('app/core/core', () => ({ + coreModule: { + directive: () => {}, + }, + appEvents: { + on: () => {}, + }, +})); + +import '../module'; +import { GraphCtrl } from '../module'; +import { MetricsPanelCtrl } from 'app/features/panel/metrics_panel_ctrl'; +import { PanelCtrl } from 'app/features/panel/panel_ctrl'; + +import config from 'app/core/config'; + +import TimeSeries from 'app/core/time_series2'; +import moment from 'moment'; +import $ from 'jquery'; +import { graphDirective } from '../graph'; + +let ctx = {}; +let ctrl; +let scope = { + ctrl: {}, + range: { + from: moment([2015, 1, 1]), + to: moment([2015, 11, 20]), + }, + $on: () => {}, +}; +let link; + +describe('grafanaGraph', function() { + const setupCtx = (beforeRender?) => { + config.bootData = { + user: { + lightTheme: false, + }, + }; + GraphCtrl.prototype = { + ...MetricsPanelCtrl.prototype, + ...PanelCtrl.prototype, + ...GraphCtrl.prototype, + height: 200, + panel: { + events: { + on: () => {}, + }, + legend: {}, + grid: {}, + yaxes: [ + { + min: null, + max: null, + format: 'short', + logBase: 1, + }, + { + min: null, + max: null, + format: 'short', + logBase: 1, + }, + ], + thresholds: [], + xaxis: {}, + seriesOverrides: [], + tooltip: { + shared: true, + }, + }, + renderingCompleted: jest.fn(), + hiddenSeries: {}, + dashboard: { + getTimezone: () => 'browser', + }, + range: { + from: moment([2015, 1, 1, 10]), + to: moment([2015, 1, 1, 22]), + }, + }; + + ctx.data = []; + ctx.data.push( + new TimeSeries({ + datapoints: [[1, 1], [2, 2]], + alias: 'series1', + }) + ); + ctx.data.push( + new TimeSeries({ + datapoints: [[10, 1], [20, 2]], + alias: 'series2', + }) + ); + + ctrl = new GraphCtrl( + { + $on: () => {}, + }, + { + get: () => {}, + }, + {} + ); + + $.plot = ctrl.plot = jest.fn(); + scope.ctrl = ctrl; + + link = graphDirective({}, {}, {}).link(scope, { width: () => 500, mouseleave: () => {}, bind: () => {} }); + if (typeof beforeRender === 'function') { + beforeRender(); + } + link.data = ctx.data; + + //Emulate functions called by event listeners + link.buildFlotPairs(link.data); + link.render_panel(); + ctx.plotData = ctrl.plot.mock.calls[0][1]; + + ctx.plotOptions = ctrl.plot.mock.calls[0][2]; + }; + + describe('simple lines options', () => { + beforeEach(() => { + setupCtx(() => { + ctrl.panel.lines = true; + ctrl.panel.fill = 5; + ctrl.panel.linewidth = 3; + ctrl.panel.steppedLine = true; + }); + }); + + it('should configure plot with correct options', () => { + expect(ctx.plotOptions.series.lines.show).toBe(true); + expect(ctx.plotOptions.series.lines.fill).toBe(0.5); + expect(ctx.plotOptions.series.lines.lineWidth).toBe(3); + expect(ctx.plotOptions.series.lines.steps).toBe(true); + }); + }); + + describe('sorting stacked series as legend. disabled', () => { + beforeEach(() => { + setupCtx(() => { + ctrl.panel.legend.sort = undefined; + ctrl.panel.stack = false; + }); + }); + + it('should not modify order of time series', () => { + expect(ctx.plotData[0].alias).toBe('series1'); + expect(ctx.plotData[1].alias).toBe('series2'); + }); + }); + + describe('sorting stacked series as legend. min descending order', () => { + beforeEach(() => { + setupCtx(() => { + ctrl.panel.legend.sort = 'min'; + ctrl.panel.legend.sortDesc = true; + ctrl.panel.stack = true; + }); + }); + it('highest value should be first', () => { + expect(ctx.plotData[0].alias).toBe('series2'); + expect(ctx.plotData[1].alias).toBe('series1'); + }); + }); + + describe('sorting stacked series as legend. min ascending order', () => { + beforeEach(() => { + setupCtx(() => { + ctrl.panel.legend.sort = 'min'; + ctrl.panel.legend.sortDesc = false; + ctrl.panel.stack = true; + }); + }); + it('lowest value should be first', () => { + expect(ctx.plotData[0].alias).toBe('series1'); + expect(ctx.plotData[1].alias).toBe('series2'); + }); + }); + + describe('sorting stacked series as legend. stacking disabled', () => { + beforeEach(() => { + setupCtx(() => { + ctrl.panel.legend.sort = 'min'; + ctrl.panel.legend.sortDesc = true; + ctrl.panel.stack = false; + }); + }); + + it('highest value should be first', () => { + expect(ctx.plotData[0].alias).toBe('series1'); + expect(ctx.plotData[1].alias).toBe('series2'); + }); + }); + + describe('sorting stacked series as legend. current descending order', () => { + beforeEach(() => { + setupCtx(() => { + ctrl.panel.legend.sort = 'current'; + ctrl.panel.legend.sortDesc = true; + ctrl.panel.stack = true; + }); + }); + + it('highest last value should be first', () => { + expect(ctx.plotData[0].alias).toBe('series2'); + expect(ctx.plotData[1].alias).toBe('series1'); + }); + }); + + describe('when logBase is log 10', () => { + beforeEach(() => { + setupCtx(() => { + ctx.data[0] = new TimeSeries({ + datapoints: [[2000, 1], [0.002, 2], [0, 3], [-1, 4]], + alias: 'seriesAutoscale', + }); + ctx.data[0].yaxis = 1; + ctx.data[1] = new TimeSeries({ + datapoints: [[2000, 1], [0.002, 2], [0, 3], [-1, 4]], + alias: 'seriesFixedscale', + }); + ctx.data[1].yaxis = 2; + ctrl.panel.yaxes[0].logBase = 10; + + ctrl.panel.yaxes[1].logBase = 10; + ctrl.panel.yaxes[1].min = '0.05'; + ctrl.panel.yaxes[1].max = '1500'; + }); + }); + + it('should apply axis transform, autoscaling (if necessary) and ticks', function() { + var axisAutoscale = ctx.plotOptions.yaxes[0]; + expect(axisAutoscale.transform(100)).toBe(2); + expect(axisAutoscale.inverseTransform(-3)).toBeCloseTo(0.001); + expect(axisAutoscale.min).toBeCloseTo(0.001); + expect(axisAutoscale.max).toBe(10000); + expect(axisAutoscale.ticks.length).toBeCloseTo(8); + expect(axisAutoscale.ticks[0]).toBeCloseTo(0.001); + if (axisAutoscale.ticks.length === 7) { + expect(axisAutoscale.ticks[axisAutoscale.ticks.length - 1]).toBeCloseTo(1000); + } else { + expect(axisAutoscale.ticks[axisAutoscale.ticks.length - 1]).toBe(10000); + } + + var axisFixedscale = ctx.plotOptions.yaxes[1]; + expect(axisFixedscale.min).toBe(0.05); + expect(axisFixedscale.max).toBe(1500); + expect(axisFixedscale.ticks.length).toBe(5); + expect(axisFixedscale.ticks[0]).toBe(0.1); + expect(axisFixedscale.ticks[4]).toBe(1000); + }); + }); + + describe('when logBase is log 10 and data points contain only zeroes', () => { + beforeEach(() => { + setupCtx(() => { + ctrl.panel.yaxes[0].logBase = 10; + ctx.data[0] = new TimeSeries({ + datapoints: [[0, 1], [0, 2], [0, 3], [0, 4]], + alias: 'seriesAutoscale', + }); + ctx.data[0].yaxis = 1; + }); + }); + + it('should not set min and max and should create some fake ticks', function() { + var axisAutoscale = ctx.plotOptions.yaxes[0]; + expect(axisAutoscale.transform(100)).toBe(2); + expect(axisAutoscale.inverseTransform(-3)).toBeCloseTo(0.001); + expect(axisAutoscale.min).toBe(undefined); + expect(axisAutoscale.max).toBe(undefined); + expect(axisAutoscale.ticks.length).toBe(2); + expect(axisAutoscale.ticks[0]).toBe(1); + expect(axisAutoscale.ticks[1]).toBe(2); + }); + }); + + // y-min set 0 is a special case for log scale, + // this approximates it by setting min to 0.1 + describe('when logBase is log 10 and y-min is set to 0 and auto min is > 0.1', () => { + beforeEach(() => { + setupCtx(() => { + ctrl.panel.yaxes[0].logBase = 10; + ctrl.panel.yaxes[0].min = '0'; + ctx.data[0] = new TimeSeries({ + datapoints: [[2000, 1], [4, 2], [500, 3], [3000, 4]], + alias: 'seriesAutoscale', + }); + ctx.data[0].yaxis = 1; + }); + }); + it('should set min to 0.1 and add a tick for 0.1', function() { + var axisAutoscale = ctx.plotOptions.yaxes[0]; + expect(axisAutoscale.transform(100)).toBe(2); + expect(axisAutoscale.inverseTransform(-3)).toBeCloseTo(0.001); + expect(axisAutoscale.min).toBe(0.1); + expect(axisAutoscale.max).toBe(10000); + expect(axisAutoscale.ticks.length).toBe(6); + expect(axisAutoscale.ticks[0]).toBe(0.1); + expect(axisAutoscale.ticks[5]).toBe(10000); + }); + }); + + describe('when logBase is log 2 and y-min is set to 0 and num of ticks exceeds max', () => { + beforeEach(() => { + setupCtx(() => { + const heightForApprox5Ticks = 125; + ctrl.height = heightForApprox5Ticks; + ctrl.panel.yaxes[0].logBase = 2; + ctrl.panel.yaxes[0].min = '0'; + ctx.data[0] = new TimeSeries({ + datapoints: [[2000, 1], [4, 2], [500, 3], [3000, 4], [10000, 5], [100000, 6]], + alias: 'seriesAutoscale', + }); + ctx.data[0].yaxis = 1; + }); + }); + + it('should regenerate ticks so that if fits on the y-axis', function() { + var axisAutoscale = ctx.plotOptions.yaxes[0]; + expect(axisAutoscale.min).toBe(0.1); + expect(axisAutoscale.ticks.length).toBe(8); + expect(axisAutoscale.ticks[0]).toBe(0.1); + expect(axisAutoscale.ticks[7]).toBe(262144); + expect(axisAutoscale.max).toBe(262144); + }); + + it('should set axis max to be max tick value', function() { + expect(ctx.plotOptions.yaxes[0].max).toBe(262144); + }); + }); + + describe('dashed lines options', () => { + beforeEach(() => { + setupCtx(() => { + ctrl.panel.lines = true; + ctrl.panel.linewidth = 2; + ctrl.panel.dashes = true; + }); + }); + + it('should configure dashed plot with correct options', function() { + expect(ctx.plotOptions.series.lines.show).toBe(true); + expect(ctx.plotOptions.series.dashes.lineWidth).toBe(2); + expect(ctx.plotOptions.series.dashes.show).toBe(true); + }); + }); + + describe('should use timeStep for barWidth', () => { + beforeEach(() => { + setupCtx(() => { + ctrl.panel.bars = true; + ctx.data[0] = new TimeSeries({ + datapoints: [[1, 10], [2, 20]], + alias: 'series1', + }); + }); + }); + + it('should set barWidth', function() { + expect(ctx.plotOptions.series.bars.barWidth).toBe(1 / 1.5); + }); + }); + + describe('series option overrides, fill & points', () => { + beforeEach(() => { + setupCtx(() => { + ctrl.panel.lines = true; + ctrl.panel.fill = 5; + ctx.data[0].zindex = 10; + ctx.data[1].alias = 'test'; + ctx.data[1].lines = { fill: 0.001 }; + ctx.data[1].points = { show: true }; + }); + }); + + it('should match second series and fill zero, and enable points', function() { + expect(ctx.plotOptions.series.lines.fill).toBe(0.5); + expect(ctx.plotData[1].lines.fill).toBe(0.001); + expect(ctx.plotData[1].points.show).toBe(true); + }); + }); + + describe('should order series order according to zindex', () => { + beforeEach(() => { + setupCtx(() => { + ctx.data[1].zindex = 1; + ctx.data[0].zindex = 10; + }); + }); + + it('should move zindex 2 last', function() { + expect(ctx.plotData[0].alias).toBe('series2'); + expect(ctx.plotData[1].alias).toBe('series1'); + }); + }); + + describe('when series is hidden', () => { + beforeEach(() => { + setupCtx(() => { + ctrl.hiddenSeries = { series2: true }; + }); + }); + + it('should remove datapoints and disable stack', function() { + expect(ctx.plotData[0].alias).toBe('series1'); + expect(ctx.plotData[1].data.length).toBe(0); + expect(ctx.plotData[1].stack).toBe(false); + }); + }); + + describe('when stack and percent', () => { + beforeEach(() => { + setupCtx(() => { + ctrl.panel.percentage = true; + ctrl.panel.stack = true; + }); + }); + + it('should show percentage', function() { + var axis = ctx.plotOptions.yaxes[0]; + expect(axis.tickFormatter(100, axis)).toBe('100%'); + }); + }); + + describe('when panel too narrow to show x-axis dates in same granularity as wide panels', () => { + //Set width to 10px + describe('and the range is less than 24 hours', function() { + beforeEach(() => { + setupCtx(() => { + ctrl.range.from = moment([2015, 1, 1, 10]); + ctrl.range.to = moment([2015, 1, 1, 22]); + }); + }); + + it('should format dates as hours minutes', function() { + var axis = ctx.plotOptions.xaxis; + expect(axis.timeformat).toBe('%H:%M'); + }); + }); + + describe('and the range is less than one year', function() { + beforeEach(() => { + setupCtx(() => { + ctrl.range.from = moment([2015, 1, 1]); + ctrl.range.to = moment([2015, 11, 20]); + }); + }); + + it('should format dates as month days', function() { + var axis = ctx.plotOptions.xaxis; + expect(axis.timeformat).toBe('%m/%d'); + }); + }); + }); + + describe('when graph is histogram, and enable stack', () => { + beforeEach(() => { + setupCtx(() => { + ctrl.panel.xaxis.mode = 'histogram'; + ctrl.panel.stack = true; + ctrl.hiddenSeries = {}; + ctx.data[0] = new TimeSeries({ + datapoints: [[100, 1], [100, 2], [200, 3], [300, 4]], + alias: 'series1', + }); + ctx.data[1] = new TimeSeries({ + datapoints: [[100, 1], [100, 2], [200, 3], [300, 4]], + alias: 'series2', + }); + }); + }); + + it('should calculate correct histogram', function() { + expect(ctx.plotData[0].data[0][0]).toBe(100); + expect(ctx.plotData[0].data[0][1]).toBe(2); + expect(ctx.plotData[1].data[0][0]).toBe(100); + expect(ctx.plotData[1].data[0][1]).toBe(2); + }); + }); + + describe('when graph is histogram, and some series are hidden', () => { + beforeEach(() => { + setupCtx(() => { + ctrl.panel.xaxis.mode = 'histogram'; + ctrl.panel.stack = false; + ctrl.hiddenSeries = { series2: true }; + ctx.data[0] = new TimeSeries({ + datapoints: [[100, 1], [100, 2], [200, 3], [300, 4]], + alias: 'series1', + }); + ctx.data[1] = new TimeSeries({ + datapoints: [[100, 1], [100, 2], [200, 3], [300, 4]], + alias: 'series2', + }); + }); + }); + + it('should calculate correct histogram', function() { + expect(ctx.plotData[0].data[0][0]).toBe(100); + expect(ctx.plotData[0].data[0][1]).toBe(2); + }); + }); +}); diff --git a/public/app/plugins/panel/graph/specs/graph_specs.ts b/public/app/plugins/panel/graph/specs/graph_specs.ts deleted file mode 100644 index d29320a9d72..00000000000 --- a/public/app/plugins/panel/graph/specs/graph_specs.ts +++ /dev/null @@ -1,454 +0,0 @@ -import { describe, beforeEach, it, sinon, expect, angularMocks } from '../../../../../test/lib/common'; - -import '../module'; -import angular from 'angular'; -import $ from 'jquery'; -import helpers from 'test/specs/helpers'; -import TimeSeries from 'app/core/time_series2'; -import moment from 'moment'; -import { Emitter } from 'app/core/core'; - -describe('grafanaGraph', function() { - beforeEach(angularMocks.module('grafana.core')); - - function graphScenario(desc, func, elementWidth = 500) { - describe(desc, () => { - var ctx: any = {}; - - ctx.setup = setupFunc => { - beforeEach( - angularMocks.module($provide => { - $provide.value('timeSrv', new helpers.TimeSrvStub()); - }) - ); - - beforeEach( - angularMocks.inject(($rootScope, $compile) => { - var ctrl: any = { - height: 200, - panel: { - events: new Emitter(), - legend: {}, - grid: {}, - yaxes: [ - { - min: null, - max: null, - format: 'short', - logBase: 1, - }, - { - min: null, - max: null, - format: 'short', - logBase: 1, - }, - ], - thresholds: [], - xaxis: {}, - seriesOverrides: [], - tooltip: { - shared: true, - }, - }, - renderingCompleted: sinon.spy(), - hiddenSeries: {}, - dashboard: { - getTimezone: sinon.stub().returns('browser'), - }, - range: { - from: moment([2015, 1, 1, 10]), - to: moment([2015, 1, 1, 22]), - }, - }; - - var scope = $rootScope.$new(); - scope.ctrl = ctrl; - scope.ctrl.events = ctrl.panel.events; - - $rootScope.onAppEvent = sinon.spy(); - - ctx.data = []; - ctx.data.push( - new TimeSeries({ - datapoints: [[1, 1], [2, 2]], - alias: 'series1', - }) - ); - ctx.data.push( - new TimeSeries({ - datapoints: [[10, 1], [20, 2]], - alias: 'series2', - }) - ); - - setupFunc(ctrl, ctx.data); - - var element = angular.element("
    "); - $compile(element)(scope); - scope.$digest(); - - $.plot = ctx.plotSpy = sinon.spy(); - ctrl.events.emit('render', ctx.data); - ctrl.events.emit('render-legend'); - ctrl.events.emit('legend-rendering-complete'); - ctx.plotData = ctx.plotSpy.getCall(0).args[1]; - ctx.plotOptions = ctx.plotSpy.getCall(0).args[2]; - }) - ); - }; - - func(ctx); - }); - } - - graphScenario('simple lines options', ctx => { - ctx.setup(ctrl => { - ctrl.panel.lines = true; - ctrl.panel.fill = 5; - ctrl.panel.linewidth = 3; - ctrl.panel.steppedLine = true; - }); - - it('should configure plot with correct options', () => { - expect(ctx.plotOptions.series.lines.show).to.be(true); - expect(ctx.plotOptions.series.lines.fill).to.be(0.5); - expect(ctx.plotOptions.series.lines.lineWidth).to.be(3); - expect(ctx.plotOptions.series.lines.steps).to.be(true); - }); - }); - - graphScenario('sorting stacked series as legend. disabled', ctx => { - ctx.setup(ctrl => { - ctrl.panel.legend.sort = undefined; - ctrl.panel.stack = false; - }); - - it('should not modify order of time series', () => { - expect(ctx.plotData[0].alias).to.be('series1'); - expect(ctx.plotData[1].alias).to.be('series2'); - }); - }); - - graphScenario('sorting stacked series as legend. min descending order', ctx => { - ctx.setup(ctrl => { - ctrl.panel.legend.sort = 'min'; - ctrl.panel.legend.sortDesc = true; - ctrl.panel.stack = true; - }); - - it('highest value should be first', () => { - expect(ctx.plotData[0].alias).to.be('series2'); - expect(ctx.plotData[1].alias).to.be('series1'); - }); - }); - - graphScenario('sorting stacked series as legend. min ascending order', ctx => { - ctx.setup((ctrl, data) => { - ctrl.panel.legend.sort = 'min'; - ctrl.panel.legend.sortDesc = false; - ctrl.panel.stack = true; - }); - - it('lowest value should be first', () => { - expect(ctx.plotData[0].alias).to.be('series1'); - expect(ctx.plotData[1].alias).to.be('series2'); - }); - }); - - graphScenario('sorting stacked series as legend. stacking disabled', ctx => { - ctx.setup(ctrl => { - ctrl.panel.legend.sort = 'min'; - ctrl.panel.legend.sortDesc = true; - ctrl.panel.stack = false; - }); - - it('highest value should be first', () => { - expect(ctx.plotData[0].alias).to.be('series1'); - expect(ctx.plotData[1].alias).to.be('series2'); - }); - }); - - graphScenario('sorting stacked series as legend. current descending order', ctx => { - ctx.setup(ctrl => { - ctrl.panel.legend.sort = 'current'; - ctrl.panel.legend.sortDesc = true; - ctrl.panel.stack = true; - }); - - it('highest last value should be first', () => { - expect(ctx.plotData[0].alias).to.be('series2'); - expect(ctx.plotData[1].alias).to.be('series1'); - }); - }); - - graphScenario('when logBase is log 10', function(ctx) { - ctx.setup(function(ctrl, data) { - ctrl.panel.yaxes[0].logBase = 10; - data[0] = new TimeSeries({ - datapoints: [[2000, 1], [0.002, 2], [0, 3], [-1, 4]], - alias: 'seriesAutoscale', - }); - data[0].yaxis = 1; - ctrl.panel.yaxes[1].logBase = 10; - ctrl.panel.yaxes[1].min = '0.05'; - ctrl.panel.yaxes[1].max = '1500'; - data[1] = new TimeSeries({ - datapoints: [[2000, 1], [0.002, 2], [0, 3], [-1, 4]], - alias: 'seriesFixedscale', - }); - data[1].yaxis = 2; - }); - - it('should apply axis transform, autoscaling (if necessary) and ticks', function() { - var axisAutoscale = ctx.plotOptions.yaxes[0]; - expect(axisAutoscale.transform(100)).to.be(2); - expect(axisAutoscale.inverseTransform(-3)).to.within(0.00099999999, 0.00100000001); - expect(axisAutoscale.min).to.within(0.00099999999, 0.00100000001); - expect(axisAutoscale.max).to.be(10000); - expect(axisAutoscale.ticks.length).to.within(7, 8); - expect(axisAutoscale.ticks[0]).to.within(0.00099999999, 0.00100000001); - if (axisAutoscale.ticks.length === 7) { - expect(axisAutoscale.ticks[axisAutoscale.ticks.length - 1]).to.within(999.9999, 1000.0001); - } else { - expect(axisAutoscale.ticks[axisAutoscale.ticks.length - 1]).to.be(10000); - } - - var axisFixedscale = ctx.plotOptions.yaxes[1]; - expect(axisFixedscale.min).to.be(0.05); - expect(axisFixedscale.max).to.be(1500); - expect(axisFixedscale.ticks.length).to.be(5); - expect(axisFixedscale.ticks[0]).to.be(0.1); - expect(axisFixedscale.ticks[4]).to.be(1000); - }); - }); - - graphScenario('when logBase is log 10 and data points contain only zeroes', function(ctx) { - ctx.setup(function(ctrl, data) { - ctrl.panel.yaxes[0].logBase = 10; - data[0] = new TimeSeries({ - datapoints: [[0, 1], [0, 2], [0, 3], [0, 4]], - alias: 'seriesAutoscale', - }); - data[0].yaxis = 1; - }); - - it('should not set min and max and should create some fake ticks', function() { - var axisAutoscale = ctx.plotOptions.yaxes[0]; - expect(axisAutoscale.transform(100)).to.be(2); - expect(axisAutoscale.inverseTransform(-3)).to.within(0.00099999999, 0.00100000001); - expect(axisAutoscale.min).to.be(undefined); - expect(axisAutoscale.max).to.be(undefined); - expect(axisAutoscale.ticks.length).to.be(2); - expect(axisAutoscale.ticks[0]).to.be(1); - expect(axisAutoscale.ticks[1]).to.be(2); - }); - }); - - // y-min set 0 is a special case for log scale, - // this approximates it by setting min to 0.1 - graphScenario('when logBase is log 10 and y-min is set to 0 and auto min is > 0.1', function(ctx) { - ctx.setup(function(ctrl, data) { - ctrl.panel.yaxes[0].logBase = 10; - ctrl.panel.yaxes[0].min = '0'; - data[0] = new TimeSeries({ - datapoints: [[2000, 1], [4, 2], [500, 3], [3000, 4]], - alias: 'seriesAutoscale', - }); - data[0].yaxis = 1; - }); - - it('should set min to 0.1 and add a tick for 0.1', function() { - var axisAutoscale = ctx.plotOptions.yaxes[0]; - expect(axisAutoscale.transform(100)).to.be(2); - expect(axisAutoscale.inverseTransform(-3)).to.within(0.00099999999, 0.00100000001); - expect(axisAutoscale.min).to.be(0.1); - expect(axisAutoscale.max).to.be(10000); - expect(axisAutoscale.ticks.length).to.be(6); - expect(axisAutoscale.ticks[0]).to.be(0.1); - expect(axisAutoscale.ticks[5]).to.be(10000); - }); - }); - - graphScenario('when logBase is log 2 and y-min is set to 0 and num of ticks exceeds max', function(ctx) { - ctx.setup(function(ctrl, data) { - const heightForApprox5Ticks = 125; - ctrl.height = heightForApprox5Ticks; - ctrl.panel.yaxes[0].logBase = 2; - ctrl.panel.yaxes[0].min = '0'; - data[0] = new TimeSeries({ - datapoints: [[2000, 1], [4, 2], [500, 3], [3000, 4], [10000, 5], [100000, 6]], - alias: 'seriesAutoscale', - }); - data[0].yaxis = 1; - }); - - it('should regenerate ticks so that if fits on the y-axis', function() { - var axisAutoscale = ctx.plotOptions.yaxes[0]; - expect(axisAutoscale.min).to.be(0.1); - expect(axisAutoscale.ticks.length).to.be(8); - expect(axisAutoscale.ticks[0]).to.be(0.1); - expect(axisAutoscale.ticks[7]).to.be(262144); - expect(axisAutoscale.max).to.be(262144); - }); - - it('should set axis max to be max tick value', function() { - expect(ctx.plotOptions.yaxes[0].max).to.be(262144); - }); - }); - - graphScenario('dashed lines options', function(ctx) { - ctx.setup(function(ctrl) { - ctrl.panel.lines = true; - ctrl.panel.linewidth = 2; - ctrl.panel.dashes = true; - }); - - it('should configure dashed plot with correct options', function() { - expect(ctx.plotOptions.series.lines.show).to.be(true); - expect(ctx.plotOptions.series.dashes.lineWidth).to.be(2); - expect(ctx.plotOptions.series.dashes.show).to.be(true); - }); - }); - - graphScenario('should use timeStep for barWidth', function(ctx) { - ctx.setup(function(ctrl, data) { - ctrl.panel.bars = true; - data[0] = new TimeSeries({ - datapoints: [[1, 10], [2, 20]], - alias: 'series1', - }); - }); - - it('should set barWidth', function() { - expect(ctx.plotOptions.series.bars.barWidth).to.be(1 / 1.5); - }); - }); - - graphScenario('series option overrides, fill & points', function(ctx) { - ctx.setup(function(ctrl, data) { - ctrl.panel.lines = true; - ctrl.panel.fill = 5; - data[0].zindex = 10; - data[1].alias = 'test'; - data[1].lines = { fill: 0.001 }; - data[1].points = { show: true }; - }); - - it('should match second series and fill zero, and enable points', function() { - expect(ctx.plotOptions.series.lines.fill).to.be(0.5); - expect(ctx.plotData[1].lines.fill).to.be(0.001); - expect(ctx.plotData[1].points.show).to.be(true); - }); - }); - - graphScenario('should order series order according to zindex', function(ctx) { - ctx.setup(function(ctrl, data) { - data[1].zindex = 1; - data[0].zindex = 10; - }); - - it('should move zindex 2 last', function() { - expect(ctx.plotData[0].alias).to.be('series2'); - expect(ctx.plotData[1].alias).to.be('series1'); - }); - }); - - graphScenario('when series is hidden', function(ctx) { - ctx.setup(function(ctrl) { - ctrl.hiddenSeries = { series2: true }; - }); - - it('should remove datapoints and disable stack', function() { - expect(ctx.plotData[0].alias).to.be('series1'); - expect(ctx.plotData[1].data.length).to.be(0); - expect(ctx.plotData[1].stack).to.be(false); - }); - }); - - graphScenario('when stack and percent', function(ctx) { - ctx.setup(function(ctrl) { - ctrl.panel.percentage = true; - ctrl.panel.stack = true; - }); - - it('should show percentage', function() { - var axis = ctx.plotOptions.yaxes[0]; - expect(axis.tickFormatter(100, axis)).to.be('100%'); - }); - }); - - graphScenario( - 'when panel too narrow to show x-axis dates in same granularity as wide panels', - function(ctx) { - describe('and the range is less than 24 hours', function() { - ctx.setup(function(ctrl) { - ctrl.range.from = moment([2015, 1, 1, 10]); - ctrl.range.to = moment([2015, 1, 1, 22]); - }); - - it('should format dates as hours minutes', function() { - var axis = ctx.plotOptions.xaxis; - expect(axis.timeformat).to.be('%H:%M'); - }); - }); - - describe('and the range is less than one year', function() { - ctx.setup(function(scope) { - scope.range.from = moment([2015, 1, 1]); - scope.range.to = moment([2015, 11, 20]); - }); - - it('should format dates as month days', function() { - var axis = ctx.plotOptions.xaxis; - expect(axis.timeformat).to.be('%m/%d'); - }); - }); - }, - 10 - ); - - graphScenario('when graph is histogram, and enable stack', function(ctx) { - ctx.setup(function(ctrl, data) { - ctrl.panel.xaxis.mode = 'histogram'; - ctrl.panel.stack = true; - ctrl.hiddenSeries = {}; - data[0] = new TimeSeries({ - datapoints: [[100, 1], [100, 2], [200, 3], [300, 4]], - alias: 'series1', - }); - data[1] = new TimeSeries({ - datapoints: [[100, 1], [100, 2], [200, 3], [300, 4]], - alias: 'series2', - }); - }); - - it('should calculate correct histogram', function() { - expect(ctx.plotData[0].data[0][0]).to.be(100); - expect(ctx.plotData[0].data[0][1]).to.be(2); - expect(ctx.plotData[1].data[0][0]).to.be(100); - expect(ctx.plotData[1].data[0][1]).to.be(2); - }); - }); - - graphScenario('when graph is histogram, and some series are hidden', function(ctx) { - ctx.setup(function(ctrl, data) { - ctrl.panel.xaxis.mode = 'histogram'; - ctrl.panel.stack = false; - ctrl.hiddenSeries = { series2: true }; - data[0] = new TimeSeries({ - datapoints: [[100, 1], [100, 2], [200, 3], [300, 4]], - alias: 'series1', - }); - data[1] = new TimeSeries({ - datapoints: [[100, 1], [100, 2], [200, 3], [300, 4]], - alias: 'series2', - }); - }); - - it('should calculate correct histogram', function() { - expect(ctx.plotData[0].data[0][0]).to.be(100); - expect(ctx.plotData[0].data[0][1]).to.be(2); - }); - }); -}); From 35694a76efbff0ebff57c1af7c6ecbc0a8365fc2 Mon Sep 17 00:00:00 2001 From: Tobias Skarhed Date: Wed, 1 Aug 2018 17:11:29 +0200 Subject: [PATCH 399/786] Class to function. Half tests passing --- .../app/features/dashboard/shareModalCtrl.ts | 180 +++++++++--------- .../dashboard/specs/share_modal_ctrl.jest.ts | 154 +++++++++++++++ 2 files changed, 243 insertions(+), 91 deletions(-) create mode 100644 public/app/features/dashboard/specs/share_modal_ctrl.jest.ts diff --git a/public/app/features/dashboard/shareModalCtrl.ts b/public/app/features/dashboard/shareModalCtrl.ts index 985c20f03b2..c32c2a79190 100644 --- a/public/app/features/dashboard/shareModalCtrl.ts +++ b/public/app/features/dashboard/shareModalCtrl.ts @@ -2,120 +2,118 @@ import angular from 'angular'; import config from 'app/core/config'; import moment from 'moment'; -export class ShareModalCtrl { - /** @ngInject */ - constructor($scope, $rootScope, $location, $timeout, timeSrv, templateSrv, linkSrv) { - $scope.options = { - forCurrent: true, - includeTemplateVars: true, - theme: 'current', - }; - $scope.editor = { index: $scope.tabIndex || 0 }; +/** @ngInject */ +export function ShareModalCtrl($scope, $rootScope, $location, $timeout, timeSrv, templateSrv, linkSrv) { + $scope.options = { + forCurrent: true, + includeTemplateVars: true, + theme: 'current', + }; + $scope.editor = { index: $scope.tabIndex || 0 }; - $scope.init = function() { - $scope.modeSharePanel = $scope.panel ? true : false; + $scope.init = function() { + $scope.modeSharePanel = $scope.panel ? true : false; - $scope.tabs = [{ title: 'Link', src: 'shareLink.html' }]; + $scope.tabs = [{ title: 'Link', src: 'shareLink.html' }]; - if ($scope.modeSharePanel) { - $scope.modalTitle = 'Share Panel'; - $scope.tabs.push({ title: 'Embed', src: 'shareEmbed.html' }); - } else { - $scope.modalTitle = 'Share'; - } + if ($scope.modeSharePanel) { + $scope.modalTitle = 'Share Panel'; + $scope.tabs.push({ title: 'Embed', src: 'shareEmbed.html' }); + } else { + $scope.modalTitle = 'Share'; + } - if (!$scope.dashboard.meta.isSnapshot) { - $scope.tabs.push({ title: 'Snapshot', src: 'shareSnapshot.html' }); - } + if (!$scope.dashboard.meta.isSnapshot) { + $scope.tabs.push({ title: 'Snapshot', src: 'shareSnapshot.html' }); + } - if (!$scope.dashboard.meta.isSnapshot && !$scope.modeSharePanel) { - $scope.tabs.push({ title: 'Export', src: 'shareExport.html' }); - } + if (!$scope.dashboard.meta.isSnapshot && !$scope.modeSharePanel) { + $scope.tabs.push({ title: 'Export', src: 'shareExport.html' }); + } - $scope.buildUrl(); - }; + $scope.buildUrl(); + }; - $scope.buildUrl = function() { - var baseUrl = $location.absUrl(); - var queryStart = baseUrl.indexOf('?'); + $scope.buildUrl = function() { + var baseUrl = $location.absUrl(); + var queryStart = baseUrl.indexOf('?'); - if (queryStart !== -1) { - baseUrl = baseUrl.substring(0, queryStart); - } + if (queryStart !== -1) { + baseUrl = baseUrl.substring(0, queryStart); + } - var params = angular.copy($location.search()); + var params = angular.copy($location.search()); - var range = timeSrv.timeRange(); - params.from = range.from.valueOf(); - params.to = range.to.valueOf(); - params.orgId = config.bootData.user.orgId; + var range = timeSrv.timeRange(); + params.from = range.from.valueOf(); + params.to = range.to.valueOf(); + params.orgId = config.bootData.user.orgId; - if ($scope.options.includeTemplateVars) { - templateSrv.fillVariableValuesForUrl(params); - } + if ($scope.options.includeTemplateVars) { + templateSrv.fillVariableValuesForUrl(params); + } - if (!$scope.options.forCurrent) { - delete params.from; - delete params.to; - } + if (!$scope.options.forCurrent) { + delete params.from; + delete params.to; + } - if ($scope.options.theme !== 'current') { - params.theme = $scope.options.theme; - } + if ($scope.options.theme !== 'current') { + params.theme = $scope.options.theme; + } - if ($scope.modeSharePanel) { - params.panelId = $scope.panel.id; - params.fullscreen = true; - } else { - delete params.panelId; - delete params.fullscreen; - } - - $scope.shareUrl = linkSrv.addParamsToUrl(baseUrl, params); - - var soloUrl = baseUrl.replace(config.appSubUrl + '/dashboard/', config.appSubUrl + '/dashboard-solo/'); - soloUrl = soloUrl.replace(config.appSubUrl + '/d/', config.appSubUrl + '/d-solo/'); + if ($scope.modeSharePanel) { + params.panelId = $scope.panel.id; + params.fullscreen = true; + } else { + delete params.panelId; delete params.fullscreen; - delete params.edit; - soloUrl = linkSrv.addParamsToUrl(soloUrl, params); + } - $scope.iframeHtml = ''; + $scope.shareUrl = linkSrv.addParamsToUrl(baseUrl, params); - $scope.imageUrl = soloUrl.replace( - config.appSubUrl + '/dashboard-solo/', - config.appSubUrl + '/render/dashboard-solo/' - ); - $scope.imageUrl = $scope.imageUrl.replace(config.appSubUrl + '/d-solo/', config.appSubUrl + '/render/d-solo/'); - $scope.imageUrl += '&width=1000&height=500' + $scope.getLocalTimeZone(); - }; + var soloUrl = baseUrl.replace(config.appSubUrl + '/dashboard/', config.appSubUrl + '/dashboard-solo/'); + soloUrl = soloUrl.replace(config.appSubUrl + '/d/', config.appSubUrl + '/d-solo/'); + delete params.fullscreen; + delete params.edit; + soloUrl = linkSrv.addParamsToUrl(soloUrl, params); - // This function will try to return the proper full name of the local timezone - // Chrome does not handle the timezone offset (but phantomjs does) - $scope.getLocalTimeZone = function() { - let utcOffset = '&tz=UTC' + encodeURIComponent(moment().format('Z')); + $scope.iframeHtml = ''; - // Older browser does not the internationalization API - if (!(window).Intl) { - return utcOffset; - } + $scope.imageUrl = soloUrl.replace( + config.appSubUrl + '/dashboard-solo/', + config.appSubUrl + '/render/dashboard-solo/' + ); + $scope.imageUrl = $scope.imageUrl.replace(config.appSubUrl + '/d-solo/', config.appSubUrl + '/render/d-solo/'); + $scope.imageUrl += '&width=1000&height=500' + $scope.getLocalTimeZone(); + }; - const dateFormat = (window).Intl.DateTimeFormat(); - if (!dateFormat.resolvedOptions) { - return utcOffset; - } + // This function will try to return the proper full name of the local timezone + // Chrome does not handle the timezone offset (but phantomjs does) + $scope.getLocalTimeZone = function() { + let utcOffset = '&tz=UTC' + encodeURIComponent(moment().format('Z')); - const options = dateFormat.resolvedOptions(); - if (!options.timeZone) { - return utcOffset; - } + // Older browser does not the internationalization API + if (!(window).Intl) { + return utcOffset; + } - return '&tz=' + encodeURIComponent(options.timeZone); - }; + const dateFormat = (window).Intl.DateTimeFormat(); + if (!dateFormat.resolvedOptions) { + return utcOffset; + } - $scope.getShareUrl = function() { - return $scope.shareUrl; - }; - } + const options = dateFormat.resolvedOptions(); + if (!options.timeZone) { + return utcOffset; + } + + return '&tz=' + encodeURIComponent(options.timeZone); + }; + + $scope.getShareUrl = function() { + return $scope.shareUrl; + }; } angular.module('grafana.controllers').controller('ShareModalCtrl', ShareModalCtrl); diff --git a/public/app/features/dashboard/specs/share_modal_ctrl.jest.ts b/public/app/features/dashboard/specs/share_modal_ctrl.jest.ts new file mode 100644 index 00000000000..47b2a2189cd --- /dev/null +++ b/public/app/features/dashboard/specs/share_modal_ctrl.jest.ts @@ -0,0 +1,154 @@ +import '../shareModalCtrl'; +import { ShareModalCtrl } from '../shareModalCtrl'; +import config from 'app/core/config'; +import { LinkSrv } from 'app/features/panellinks/link_srv'; + +describe('ShareModalCtrl', () => { + var ctx = { + timeSrv: { + timeRange: () => { + return { from: new Date(1000), to: new Date(2000) }; + }, + }, + $location: { + absUrl: () => 'http://server/#!/test', + search: () => { + return { from: '', to: '' }; + }, + }, + scope: { + dashboard: { + meta: { + isSnapshot: true, + }, + }, + }, + templateSrv: { + fillVariableValuesForUrl: () => {}, + }, + }; + // function setTime(range) { + // ctx.timeSrv.timeRange = () => range; + // } + + beforeEach(() => { + config.bootData = { + user: { + orgId: 1, + }, + }; + }); + + // setTime({ from: new Date(1000), to: new Date(2000) }); + + // beforeEach(angularMocks.module('grafana.controllers')); + // beforeEach(angularMocks.module('grafana.services')); + // beforeEach( + // angularMocks.module(function($compileProvider) { + // $compileProvider.preAssignBindingsEnabled(true); + // }) + // ); + + // beforeEach(ctx.providePhase()); + + // beforeEach(ctx.createControllerPhase('ShareModalCtrl')); + beforeEach(() => { + ctx.ctrl = new ShareModalCtrl( + ctx.scope, + {}, + ctx.$location, + {}, + ctx.timeSrv, + ctx.templateSrv, + new LinkSrv({}, ctx.stimeSrv) + ); + }); + + describe('shareUrl with current time range and panel', () => { + it('should generate share url absolute time', () => { + // ctx.$location.path('/test'); + ctx.scope.panel = { id: 22 }; + + ctx.scope.init(); + expect(ctx.scope.shareUrl).toBe('http://server/#!/test?from=1000&to=2000&orgId=1&panelId=22&fullscreen'); + }); + + it('should generate render url', () => { + ctx.$location.absUrl = () => 'http://dashboards.grafana.com/d/abcdefghi/my-dash'; + + ctx.scope.panel = { id: 22 }; + + ctx.scope.init(); + var base = 'http://dashboards.grafana.com/render/d-solo/abcdefghi/my-dash'; + var params = '?from=1000&to=2000&orgId=1&panelId=22&width=1000&height=500&tz=UTC'; + expect(ctx.scope.imageUrl).toContain(base + params); + }); + + it('should generate render url for scripted dashboard', () => { + ctx.$location.absUrl = () => 'http://dashboards.grafana.com/dashboard/script/my-dash.js'; + + ctx.scope.panel = { id: 22 }; + + ctx.scope.init(); + var base = 'http://dashboards.grafana.com/render/dashboard-solo/script/my-dash.js'; + var params = '?from=1000&to=2000&orgId=1&panelId=22&width=1000&height=500&tz=UTC'; + expect(ctx.scope.imageUrl).toContain(base + params); + }); + + it('should remove panel id when no panel in scope', () => { + // ctx.$location.path('/test'); + ctx.$location.absUrl = () => 'http://server/#!/test'; + ctx.scope.options.forCurrent = true; + ctx.scope.panel = null; + + ctx.scope.init(); + expect(ctx.scope.shareUrl).toBe('http://server/#!/test?from=1000&to=2000&orgId=1'); + }); + + it('should add theme when specified', () => { + // ctx.$location.path('/test'); + ctx.scope.options.theme = 'light'; + ctx.scope.panel = null; + + ctx.scope.init(); + expect(ctx.scope.shareUrl).toBe('http://server/#!/test?from=1000&to=2000&orgId=1&theme=light'); + }); + + it('should remove fullscreen from image url when is first param in querystring and modeSharePanel is true', () => { + ctx.$location.absUrl = () => 'http://server/#!/test?fullscreen&edit'; + ctx.scope.modeSharePanel = true; + ctx.scope.panel = { id: 1 }; + + ctx.scope.buildUrl(); + + expect(ctx.scope.shareUrl).toContain('?fullscreen&edit&from=1000&to=2000&orgId=1&panelId=1'); + expect(ctx.scope.imageUrl).toContain('?from=1000&to=2000&orgId=1&panelId=1&width=1000&height=500&tz=UTC'); + }); + + it('should remove edit from image url when is first param in querystring and modeSharePanel is true', () => { + ctx.$location.absUrl = () => 'http://server/#!/test?edit&fullscreen'; + ctx.scope.modeSharePanel = true; + ctx.scope.panel = { id: 1 }; + + ctx.scope.buildUrl(); + + expect(ctx.scope.shareUrl).toContain('?edit&fullscreen&from=1000&to=2000&orgId=1&panelId=1'); + expect(ctx.scope.imageUrl).toContain('?from=1000&to=2000&orgId=1&panelId=1&width=1000&height=500&tz=UTC'); + }); + + it('should include template variables in url', () => { + ctx.$location.absUrl = () => 'http://server/#!/test'; + ctx.scope.options.includeTemplateVars = true; + + ctx.templateSrv.fillVariableValuesForUrl = function(params) { + params['var-app'] = 'mupp'; + params['var-server'] = 'srv-01'; + }; + + ctx.scope.buildUrl(); + expect(ctx.scope.shareUrl).toContain( + 'http://server/#!/test?from=1000&to=2000&orgId=1&var-app=mupp&var-server=srv-01' + ); + }); + }); +}); From 38422ce8a4128da4c4ff7370d5a3e7becaf0e588 Mon Sep 17 00:00:00 2001 From: Tobias Skarhed Date: Fri, 3 Aug 2018 14:37:31 +0200 Subject: [PATCH 400/786] All tests passing --- .../dashboard/specs/share_modal_ctrl.jest.ts | 21 +++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/public/app/features/dashboard/specs/share_modal_ctrl.jest.ts b/public/app/features/dashboard/specs/share_modal_ctrl.jest.ts index 47b2a2189cd..31f09a6c08a 100644 --- a/public/app/features/dashboard/specs/share_modal_ctrl.jest.ts +++ b/public/app/features/dashboard/specs/share_modal_ctrl.jest.ts @@ -27,6 +27,14 @@ describe('ShareModalCtrl', () => { fillVariableValuesForUrl: () => {}, }, }; + + (window).Intl.DateTimeFormat = () => { + return { + resolvedOptions: () => { + return { timeZone: 'UTC' }; + }, + }; + }; // function setTime(range) { // ctx.timeSrv.timeRange = () => range; // } @@ -48,10 +56,6 @@ describe('ShareModalCtrl', () => { // $compileProvider.preAssignBindingsEnabled(true); // }) // ); - - // beforeEach(ctx.providePhase()); - - // beforeEach(ctx.createControllerPhase('ShareModalCtrl')); beforeEach(() => { ctx.ctrl = new ShareModalCtrl( ctx.scope, @@ -115,6 +119,9 @@ describe('ShareModalCtrl', () => { }); it('should remove fullscreen from image url when is first param in querystring and modeSharePanel is true', () => { + ctx.$location.search = () => { + return { fullscreen: true, edit: true }; + }; ctx.$location.absUrl = () => 'http://server/#!/test?fullscreen&edit'; ctx.scope.modeSharePanel = true; ctx.scope.panel = { id: 1 }; @@ -126,6 +133,9 @@ describe('ShareModalCtrl', () => { }); it('should remove edit from image url when is first param in querystring and modeSharePanel is true', () => { + ctx.$location.search = () => { + return { edit: true, fullscreen: true }; + }; ctx.$location.absUrl = () => 'http://server/#!/test?edit&fullscreen'; ctx.scope.modeSharePanel = true; ctx.scope.panel = { id: 1 }; @@ -137,6 +147,9 @@ describe('ShareModalCtrl', () => { }); it('should include template variables in url', () => { + ctx.$location.search = () => { + return {}; + }; ctx.$location.absUrl = () => 'http://server/#!/test'; ctx.scope.options.includeTemplateVars = true; From be7b663369386689a62801b06bfaaafabaff8e52 Mon Sep 17 00:00:00 2001 From: Tobias Skarhed Date: Fri, 3 Aug 2018 14:40:44 +0200 Subject: [PATCH 401/786] Cleanup --- .../dashboard/specs/share_modal_ctrl.jest.ts | 16 --- .../dashboard/specs/share_modal_ctrl_specs.ts | 122 ------------------ 2 files changed, 138 deletions(-) delete mode 100644 public/app/features/dashboard/specs/share_modal_ctrl_specs.ts diff --git a/public/app/features/dashboard/specs/share_modal_ctrl.jest.ts b/public/app/features/dashboard/specs/share_modal_ctrl.jest.ts index 31f09a6c08a..e5b5340aca5 100644 --- a/public/app/features/dashboard/specs/share_modal_ctrl.jest.ts +++ b/public/app/features/dashboard/specs/share_modal_ctrl.jest.ts @@ -35,9 +35,6 @@ describe('ShareModalCtrl', () => { }, }; }; - // function setTime(range) { - // ctx.timeSrv.timeRange = () => range; - // } beforeEach(() => { config.bootData = { @@ -45,18 +42,7 @@ describe('ShareModalCtrl', () => { orgId: 1, }, }; - }); - // setTime({ from: new Date(1000), to: new Date(2000) }); - - // beforeEach(angularMocks.module('grafana.controllers')); - // beforeEach(angularMocks.module('grafana.services')); - // beforeEach( - // angularMocks.module(function($compileProvider) { - // $compileProvider.preAssignBindingsEnabled(true); - // }) - // ); - beforeEach(() => { ctx.ctrl = new ShareModalCtrl( ctx.scope, {}, @@ -100,7 +86,6 @@ describe('ShareModalCtrl', () => { }); it('should remove panel id when no panel in scope', () => { - // ctx.$location.path('/test'); ctx.$location.absUrl = () => 'http://server/#!/test'; ctx.scope.options.forCurrent = true; ctx.scope.panel = null; @@ -110,7 +95,6 @@ describe('ShareModalCtrl', () => { }); it('should add theme when specified', () => { - // ctx.$location.path('/test'); ctx.scope.options.theme = 'light'; ctx.scope.panel = null; diff --git a/public/app/features/dashboard/specs/share_modal_ctrl_specs.ts b/public/app/features/dashboard/specs/share_modal_ctrl_specs.ts deleted file mode 100644 index fc70a54a41c..00000000000 --- a/public/app/features/dashboard/specs/share_modal_ctrl_specs.ts +++ /dev/null @@ -1,122 +0,0 @@ -import { describe, beforeEach, it, expect, sinon, angularMocks } from 'test/lib/common'; -import helpers from 'test/specs/helpers'; -import '../shareModalCtrl'; -import config from 'app/core/config'; -import 'app/features/panellinks/link_srv'; - -describe('ShareModalCtrl', function() { - var ctx = new helpers.ControllerTestContext(); - - function setTime(range) { - ctx.timeSrv.timeRange = sinon.stub().returns(range); - } - - beforeEach(function() { - config.bootData = { - user: { - orgId: 1, - }, - }; - }); - - setTime({ from: new Date(1000), to: new Date(2000) }); - - beforeEach(angularMocks.module('grafana.controllers')); - beforeEach(angularMocks.module('grafana.services')); - beforeEach( - angularMocks.module(function($compileProvider) { - $compileProvider.preAssignBindingsEnabled(true); - }) - ); - - beforeEach(ctx.providePhase()); - - beforeEach(ctx.createControllerPhase('ShareModalCtrl')); - - describe('shareUrl with current time range and panel', function() { - it('should generate share url absolute time', function() { - ctx.$location.path('/test'); - ctx.scope.panel = { id: 22 }; - - ctx.scope.init(); - expect(ctx.scope.shareUrl).to.be('http://server/#!/test?from=1000&to=2000&orgId=1&panelId=22&fullscreen'); - }); - - it('should generate render url', function() { - ctx.$location.$$absUrl = 'http://dashboards.grafana.com/d/abcdefghi/my-dash'; - - ctx.scope.panel = { id: 22 }; - - ctx.scope.init(); - var base = 'http://dashboards.grafana.com/render/d-solo/abcdefghi/my-dash'; - var params = '?from=1000&to=2000&orgId=1&panelId=22&width=1000&height=500&tz=UTC'; - expect(ctx.scope.imageUrl).to.contain(base + params); - }); - - it('should generate render url for scripted dashboard', function() { - ctx.$location.$$absUrl = 'http://dashboards.grafana.com/dashboard/script/my-dash.js'; - - ctx.scope.panel = { id: 22 }; - - ctx.scope.init(); - var base = 'http://dashboards.grafana.com/render/dashboard-solo/script/my-dash.js'; - var params = '?from=1000&to=2000&orgId=1&panelId=22&width=1000&height=500&tz=UTC'; - expect(ctx.scope.imageUrl).to.contain(base + params); - }); - - it('should remove panel id when no panel in scope', function() { - ctx.$location.path('/test'); - ctx.scope.options.forCurrent = true; - ctx.scope.panel = null; - - ctx.scope.init(); - expect(ctx.scope.shareUrl).to.be('http://server/#!/test?from=1000&to=2000&orgId=1'); - }); - - it('should add theme when specified', function() { - ctx.$location.path('/test'); - ctx.scope.options.theme = 'light'; - ctx.scope.panel = null; - - ctx.scope.init(); - expect(ctx.scope.shareUrl).to.be('http://server/#!/test?from=1000&to=2000&orgId=1&theme=light'); - }); - - it('should remove fullscreen from image url when is first param in querystring and modeSharePanel is true', function() { - ctx.$location.url('/test?fullscreen&edit'); - ctx.scope.modeSharePanel = true; - ctx.scope.panel = { id: 1 }; - - ctx.scope.buildUrl(); - - expect(ctx.scope.shareUrl).to.contain('?fullscreen&edit&from=1000&to=2000&orgId=1&panelId=1'); - expect(ctx.scope.imageUrl).to.contain('?from=1000&to=2000&orgId=1&panelId=1&width=1000&height=500&tz=UTC'); - }); - - it('should remove edit from image url when is first param in querystring and modeSharePanel is true', function() { - ctx.$location.url('/test?edit&fullscreen'); - ctx.scope.modeSharePanel = true; - ctx.scope.panel = { id: 1 }; - - ctx.scope.buildUrl(); - - expect(ctx.scope.shareUrl).to.contain('?edit&fullscreen&from=1000&to=2000&orgId=1&panelId=1'); - expect(ctx.scope.imageUrl).to.contain('?from=1000&to=2000&orgId=1&panelId=1&width=1000&height=500&tz=UTC'); - }); - - it('should include template variables in url', function() { - ctx.$location.path('/test'); - ctx.scope.options.includeTemplateVars = true; - - ctx.templateSrv.fillVariableValuesForUrl = function(params) { - params['var-app'] = 'mupp'; - params['var-server'] = 'srv-01'; - }; - - ctx.scope.buildUrl(); - expect(ctx.scope.shareUrl).to.be( - 'http://server/#!/test?from=1000&to=2000&orgId=1&var-app=mupp&var-server=srv-01' - ); - }); - }); -}); From fa6d25af72f8191dc67f2948c6748def69b1a8c1 Mon Sep 17 00:00:00 2001 From: Tobias Skarhed Date: Fri, 3 Aug 2018 14:44:40 +0200 Subject: [PATCH 402/786] Remove comment --- public/app/features/dashboard/specs/share_modal_ctrl.jest.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/public/app/features/dashboard/specs/share_modal_ctrl.jest.ts b/public/app/features/dashboard/specs/share_modal_ctrl.jest.ts index e5b5340aca5..35261256566 100644 --- a/public/app/features/dashboard/specs/share_modal_ctrl.jest.ts +++ b/public/app/features/dashboard/specs/share_modal_ctrl.jest.ts @@ -56,7 +56,6 @@ describe('ShareModalCtrl', () => { describe('shareUrl with current time range and panel', () => { it('should generate share url absolute time', () => { - // ctx.$location.path('/test'); ctx.scope.panel = { id: 22 }; ctx.scope.init(); From 2459b177f914a12438424cf068638b9ce107d115 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 13 Aug 2018 18:09:01 +0200 Subject: [PATCH 403/786] change: Set User-Agent to Grafana/%Version% Proxied-DS-Request %DS-Type% in all proxied ds requests --- pkg/api/pluginproxy/ds_proxy.go | 1 + 1 file changed, 1 insertion(+) diff --git a/pkg/api/pluginproxy/ds_proxy.go b/pkg/api/pluginproxy/ds_proxy.go index b420398f9a9..74ad4e226fd 100644 --- a/pkg/api/pluginproxy/ds_proxy.go +++ b/pkg/api/pluginproxy/ds_proxy.go @@ -203,6 +203,7 @@ func (proxy *DataSourceProxy) getDirector() func(req *http.Request) { req.Header.Del("X-Forwarded-Host") req.Header.Del("X-Forwarded-Port") req.Header.Del("X-Forwarded-Proto") + req.Header.Set("User-Agent", fmt.Sprintf("Grafana/%s Proxied-DS-Request %s", setting.BuildVersion, proxy.ds.Type)) // set X-Forwarded-For header if req.RemoteAddr != "" { From 3552a4cb86151c91ecbf0b2d3265761b276dbaa6 Mon Sep 17 00:00:00 2001 From: Sven Klemm Date: Tue, 14 Aug 2018 08:34:20 +0200 Subject: [PATCH 404/786] refactor timescaledb handling in MacroEngine --- pkg/tsdb/postgres/macros.go | 15 +++++++++------ pkg/tsdb/postgres/macros_test.go | 14 ++++++++------ pkg/tsdb/postgres/postgres.go | 2 +- pkg/tsdb/postgres/postgres_test.go | 22 ---------------------- 4 files changed, 18 insertions(+), 35 deletions(-) diff --git a/pkg/tsdb/postgres/macros.go b/pkg/tsdb/postgres/macros.go index d9f97e9262c..81b0da9fbce 100644 --- a/pkg/tsdb/postgres/macros.go +++ b/pkg/tsdb/postgres/macros.go @@ -7,6 +7,7 @@ import ( "strings" "time" + "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/tsdb" ) @@ -15,12 +16,15 @@ const rsIdentifier = `([_a-zA-Z0-9]+)` const sExpr = `\$` + rsIdentifier + `\(([^\)]*)\)` type postgresMacroEngine struct { - timeRange *tsdb.TimeRange - query *tsdb.Query + timeRange *tsdb.TimeRange + query *tsdb.Query + timescaledb bool } -func newPostgresMacroEngine() tsdb.SqlMacroEngine { - return &postgresMacroEngine{} +func newPostgresMacroEngine(datasource *models.DataSource) tsdb.SqlMacroEngine { + engine := &postgresMacroEngine{} + engine.timescaledb = datasource.JsonData.Get("timescaledb").MustBool(false) + return engine } func (m *postgresMacroEngine) Interpolate(query *tsdb.Query, timeRange *tsdb.TimeRange, sql string) (string, error) { @@ -131,7 +135,7 @@ func (m *postgresMacroEngine) evaluateMacro(name string, args []string) (string, } } - if m.query.DataSource.JsonData.Get("timescaledb").MustBool() { + if m.timescaledb { return fmt.Sprintf("time_bucket('%vs',%s)", interval.Seconds(), args[0]), nil } else { return fmt.Sprintf("floor(extract(epoch from %s)/%v)*%v", args[0], interval.Seconds(), interval.Seconds()), nil @@ -142,7 +146,6 @@ func (m *postgresMacroEngine) evaluateMacro(name string, args []string) (string, return tg + " AS \"time\"", err } return "", err - case "__unixEpochFilter": if len(args) == 0 { return "", fmt.Errorf("missing time column argument for macro %v", name) diff --git a/pkg/tsdb/postgres/macros_test.go b/pkg/tsdb/postgres/macros_test.go index 449331224c2..fe95535fe0c 100644 --- a/pkg/tsdb/postgres/macros_test.go +++ b/pkg/tsdb/postgres/macros_test.go @@ -14,10 +14,12 @@ import ( func TestMacroEngine(t *testing.T) { Convey("MacroEngine", t, func() { - engine := newPostgresMacroEngine() - query := &tsdb.Query{DataSource: &models.DataSource{JsonData: simplejson.New()}} - queryTS := &tsdb.Query{DataSource: &models.DataSource{JsonData: simplejson.New()}} - queryTS.DataSource.JsonData.Set("timescaledb", true) + datasource := &models.DataSource{JsonData: simplejson.New()} + engine := newPostgresMacroEngine(datasource) + datasourceTS := &models.DataSource{JsonData: simplejson.New()} + datasourceTS.JsonData.Set("timescaledb", true) + engineTS := newPostgresMacroEngine(datasourceTS) + query := &tsdb.Query{} Convey("Given a time range between 2018-04-12 00:00 and 2018-04-12 00:05", func() { from := time.Date(2018, 4, 12, 18, 0, 0, 0, time.UTC) @@ -89,7 +91,7 @@ func TestMacroEngine(t *testing.T) { Convey("interpolate __timeGroup function with TimescaleDB enabled", func() { - sql, err := engine.Interpolate(queryTS, timeRange, "GROUP BY $__timeGroup(time_column,'5m')") + sql, err := engineTS.Interpolate(query, timeRange, "GROUP BY $__timeGroup(time_column,'5m')") So(err, ShouldBeNil) So(sql, ShouldEqual, "GROUP BY time_bucket('300s',time_column)") @@ -97,7 +99,7 @@ func TestMacroEngine(t *testing.T) { Convey("interpolate __timeGroup function with spaces between args and TimescaleDB enabled", func() { - sql, err := engine.Interpolate(queryTS, timeRange, "GROUP BY $__timeGroup(time_column , '5m')") + sql, err := engineTS.Interpolate(query, timeRange, "GROUP BY $__timeGroup(time_column , '5m')") So(err, ShouldBeNil) So(sql, ShouldEqual, "GROUP BY time_bucket('300s',time_column)") diff --git a/pkg/tsdb/postgres/postgres.go b/pkg/tsdb/postgres/postgres.go index b9f333db127..46d766f9a11 100644 --- a/pkg/tsdb/postgres/postgres.go +++ b/pkg/tsdb/postgres/postgres.go @@ -32,7 +32,7 @@ func newPostgresQueryEndpoint(datasource *models.DataSource) (tsdb.TsdbQueryEndp log: logger, } - return tsdb.NewSqlQueryEndpoint(&config, &rowTransformer, newPostgresMacroEngine(), logger) + return tsdb.NewSqlQueryEndpoint(&config, &rowTransformer, newPostgresMacroEngine(datasource), logger) } func generateConnectionString(datasource *models.DataSource) string { diff --git a/pkg/tsdb/postgres/postgres_test.go b/pkg/tsdb/postgres/postgres_test.go index 87b7f916ca9..4e05f676682 100644 --- a/pkg/tsdb/postgres/postgres_test.go +++ b/pkg/tsdb/postgres/postgres_test.go @@ -102,7 +102,6 @@ func TestPostgres(t *testing.T) { query := &tsdb.TsdbQuery{ Queries: []*tsdb.Query{ { - DataSource: &models.DataSource{JsonData: simplejson.New()}, Model: simplejson.NewFromAny(map[string]interface{}{ "rawSql": "SELECT * FROM postgres_types", "format": "table", @@ -183,7 +182,6 @@ func TestPostgres(t *testing.T) { query := &tsdb.TsdbQuery{ Queries: []*tsdb.Query{ { - DataSource: &models.DataSource{JsonData: simplejson.New()}, Model: simplejson.NewFromAny(map[string]interface{}{ "rawSql": "SELECT $__timeGroup(time, '5m') AS time, avg(value) as value FROM metric GROUP BY 1 ORDER BY 1", "format": "time_series", @@ -228,7 +226,6 @@ func TestPostgres(t *testing.T) { query := &tsdb.TsdbQuery{ Queries: []*tsdb.Query{ { - DataSource: &models.DataSource{JsonData: simplejson.New()}, Model: simplejson.NewFromAny(map[string]interface{}{ "rawSql": "SELECT $__timeGroup(time, '5m', NULL) AS time, avg(value) as value FROM metric GROUP BY 1 ORDER BY 1", "format": "time_series", @@ -283,7 +280,6 @@ func TestPostgres(t *testing.T) { query := &tsdb.TsdbQuery{ Queries: []*tsdb.Query{ { - DataSource: &models.DataSource{JsonData: simplejson.New()}, Model: simplejson.NewFromAny(map[string]interface{}{ "rawSql": "SELECT $__timeGroup(time, '5m', 1.5) AS time, avg(value) as value FROM metric GROUP BY 1 ORDER BY 1", "format": "time_series", @@ -311,7 +307,6 @@ func TestPostgres(t *testing.T) { query := &tsdb.TsdbQuery{ Queries: []*tsdb.Query{ { - DataSource: &models.DataSource{JsonData: simplejson.New()}, Model: simplejson.NewFromAny(map[string]interface{}{ "rawSql": "SELECT $__timeGroup(time, '5m', previous), avg(value) as value FROM metric GROUP BY 1 ORDER BY 1", "format": "time_series", @@ -406,7 +401,6 @@ func TestPostgres(t *testing.T) { query := &tsdb.TsdbQuery{ Queries: []*tsdb.Query{ { - DataSource: &models.DataSource{JsonData: simplejson.New()}, Model: simplejson.NewFromAny(map[string]interface{}{ "rawSql": `SELECT "timeInt64" as time, "timeInt64" FROM metric_values ORDER BY time LIMIT 1`, "format": "time_series", @@ -429,7 +423,6 @@ func TestPostgres(t *testing.T) { query := &tsdb.TsdbQuery{ Queries: []*tsdb.Query{ { - DataSource: &models.DataSource{JsonData: simplejson.New()}, Model: simplejson.NewFromAny(map[string]interface{}{ "rawSql": `SELECT "timeInt64Nullable" as time, "timeInt64Nullable" FROM metric_values ORDER BY time LIMIT 1`, "format": "time_series", @@ -452,7 +445,6 @@ func TestPostgres(t *testing.T) { query := &tsdb.TsdbQuery{ Queries: []*tsdb.Query{ { - DataSource: &models.DataSource{JsonData: simplejson.New()}, Model: simplejson.NewFromAny(map[string]interface{}{ "rawSql": `SELECT "timeFloat64" as time, "timeFloat64" FROM metric_values ORDER BY time LIMIT 1`, "format": "time_series", @@ -475,7 +467,6 @@ func TestPostgres(t *testing.T) { query := &tsdb.TsdbQuery{ Queries: []*tsdb.Query{ { - DataSource: &models.DataSource{JsonData: simplejson.New()}, Model: simplejson.NewFromAny(map[string]interface{}{ "rawSql": `SELECT "timeFloat64Nullable" as time, "timeFloat64Nullable" FROM metric_values ORDER BY time LIMIT 1`, "format": "time_series", @@ -520,7 +511,6 @@ func TestPostgres(t *testing.T) { query := &tsdb.TsdbQuery{ Queries: []*tsdb.Query{ { - DataSource: &models.DataSource{JsonData: simplejson.New()}, Model: simplejson.NewFromAny(map[string]interface{}{ "rawSql": `SELECT "timeInt32Nullable" as time, "timeInt32Nullable" FROM metric_values ORDER BY time LIMIT 1`, "format": "time_series", @@ -543,7 +533,6 @@ func TestPostgres(t *testing.T) { query := &tsdb.TsdbQuery{ Queries: []*tsdb.Query{ { - DataSource: &models.DataSource{JsonData: simplejson.New()}, Model: simplejson.NewFromAny(map[string]interface{}{ "rawSql": `SELECT "timeFloat32" as time, "timeFloat32" FROM metric_values ORDER BY time LIMIT 1`, "format": "time_series", @@ -566,7 +555,6 @@ func TestPostgres(t *testing.T) { query := &tsdb.TsdbQuery{ Queries: []*tsdb.Query{ { - DataSource: &models.DataSource{JsonData: simplejson.New()}, Model: simplejson.NewFromAny(map[string]interface{}{ "rawSql": `SELECT "timeFloat32Nullable" as time, "timeFloat32Nullable" FROM metric_values ORDER BY time LIMIT 1`, "format": "time_series", @@ -589,7 +577,6 @@ func TestPostgres(t *testing.T) { query := &tsdb.TsdbQuery{ Queries: []*tsdb.Query{ { - DataSource: &models.DataSource{JsonData: simplejson.New()}, Model: simplejson.NewFromAny(map[string]interface{}{ "rawSql": `SELECT $__timeEpoch(time), measurement || ' - value one' as metric, "valueOne" FROM metric_values ORDER BY 1`, "format": "time_series", @@ -638,7 +625,6 @@ func TestPostgres(t *testing.T) { query := &tsdb.TsdbQuery{ Queries: []*tsdb.Query{ { - DataSource: &models.DataSource{JsonData: simplejson.New()}, Model: simplejson.NewFromAny(map[string]interface{}{ "rawSql": `SELECT $__timeEpoch(time), "valueOne", "valueTwo" FROM metric_values ORDER BY 1`, "format": "time_series", @@ -696,7 +682,6 @@ func TestPostgres(t *testing.T) { query := &tsdb.TsdbQuery{ Queries: []*tsdb.Query{ { - DataSource: &models.DataSource{JsonData: simplejson.New()}, Model: simplejson.NewFromAny(map[string]interface{}{ "rawSql": `SELECT "time_sec" as time, description as text, tags FROM event WHERE $__unixEpochFilter(time_sec) AND tags='deploy' ORDER BY 1 ASC`, "format": "table", @@ -720,7 +705,6 @@ func TestPostgres(t *testing.T) { query := &tsdb.TsdbQuery{ Queries: []*tsdb.Query{ { - DataSource: &models.DataSource{JsonData: simplejson.New()}, Model: simplejson.NewFromAny(map[string]interface{}{ "rawSql": `SELECT "time_sec" as time, description as text, tags FROM event WHERE $__unixEpochFilter(time_sec) AND tags='ticket' ORDER BY 1 ASC`, "format": "table", @@ -747,7 +731,6 @@ func TestPostgres(t *testing.T) { query := &tsdb.TsdbQuery{ Queries: []*tsdb.Query{ { - DataSource: &models.DataSource{JsonData: simplejson.New()}, Model: simplejson.NewFromAny(map[string]interface{}{ "rawSql": fmt.Sprintf(`SELECT CAST('%s' AS TIMESTAMP) as time, @@ -778,7 +761,6 @@ func TestPostgres(t *testing.T) { query := &tsdb.TsdbQuery{ Queries: []*tsdb.Query{ { - DataSource: &models.DataSource{JsonData: simplejson.New()}, Model: simplejson.NewFromAny(map[string]interface{}{ "rawSql": fmt.Sprintf(`SELECT %d as time, @@ -809,7 +791,6 @@ func TestPostgres(t *testing.T) { query := &tsdb.TsdbQuery{ Queries: []*tsdb.Query{ { - DataSource: &models.DataSource{JsonData: simplejson.New()}, Model: simplejson.NewFromAny(map[string]interface{}{ "rawSql": fmt.Sprintf(`SELECT cast(%d as bigint) as time, @@ -840,7 +821,6 @@ func TestPostgres(t *testing.T) { query := &tsdb.TsdbQuery{ Queries: []*tsdb.Query{ { - DataSource: &models.DataSource{JsonData: simplejson.New()}, Model: simplejson.NewFromAny(map[string]interface{}{ "rawSql": fmt.Sprintf(`SELECT %d as time, @@ -869,7 +849,6 @@ func TestPostgres(t *testing.T) { query := &tsdb.TsdbQuery{ Queries: []*tsdb.Query{ { - DataSource: &models.DataSource{JsonData: simplejson.New()}, Model: simplejson.NewFromAny(map[string]interface{}{ "rawSql": `SELECT cast(null as bigint) as time, @@ -898,7 +877,6 @@ func TestPostgres(t *testing.T) { query := &tsdb.TsdbQuery{ Queries: []*tsdb.Query{ { - DataSource: &models.DataSource{JsonData: simplejson.New()}, Model: simplejson.NewFromAny(map[string]interface{}{ "rawSql": `SELECT cast(null as timestamp) as time, From 277a696fa577f307da16a45048261b7850e20ca7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Tue, 14 Aug 2018 08:49:56 +0200 Subject: [PATCH 405/786] fix: added missing ini default keys, fixes #12800 (#12912) --- conf/defaults.ini | 3 +++ conf/sample.ini | 4 ++++ 2 files changed, 7 insertions(+) diff --git a/conf/defaults.ini b/conf/defaults.ini index b0caed81e90..99c1537eb95 100644 --- a/conf/defaults.ini +++ b/conf/defaults.ini @@ -315,6 +315,9 @@ api_url = team_ids = allowed_organizations = tls_skip_verify_insecure = false +tls_client_cert = +tls_client_key = +tls_client_ca = #################################### Basic Auth ########################## [auth.basic] diff --git a/conf/sample.ini b/conf/sample.ini index 87544a5ac39..4291071e026 100644 --- a/conf/sample.ini +++ b/conf/sample.ini @@ -272,6 +272,10 @@ log_queries = ;api_url = https://foo.bar/user ;team_ids = ;allowed_organizations = +;tls_skip_verify_insecure = false +;tls_client_cert = +;tls_client_key = +;tls_client_ca = #################################### Grafana.com Auth #################### [auth.grafana_com] From 36e808834d8aa32364663a22a977f6462567a2ae Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Tue, 14 Aug 2018 08:50:22 +0200 Subject: [PATCH 406/786] don't render hidden columns in table panel (#12911) --- public/app/plugins/panel/table/module.html | 2 +- public/app/plugins/panel/table/renderer.ts | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/public/app/plugins/panel/table/module.html b/public/app/plugins/panel/table/module.html index 5c6fcbfdb1e..e328cb09a75 100644 --- a/public/app/plugins/panel/table/module.html +++ b/public/app/plugins/panel/table/module.html @@ -5,7 +5,7 @@
    - @@ -53,7 +53,7 @@ export class TeamGroupSync extends React.Component { this.setState({ isAdding: false, newGroupId: '' }); }; - onRemoveGroup = (group: ITeamGroup) => { + onRemoveGroup = (group: TeamGroup) => { this.props.team.removeGroup(group.groupId); }; diff --git a/public/app/containers/Teams/TeamList.tsx b/public/app/containers/Teams/TeamList.tsx index 31406250cb3..2d037eed642 100644 --- a/public/app/containers/Teams/TeamList.tsx +++ b/public/app/containers/Teams/TeamList.tsx @@ -3,7 +3,7 @@ import { hot } from 'react-hot-loader'; import { inject, observer } from 'mobx-react'; import PageHeader from 'app/core/components/PageHeader/PageHeader'; import { NavStore } from 'app/stores/NavStore/NavStore'; -import { TeamsStore, ITeam } from 'app/stores/TeamsStore/TeamsStore'; +import { TeamsStore, Team } from 'app/stores/TeamsStore/TeamsStore'; import { BackendSrv } from 'app/core/services/backend_srv'; import DeleteButton from 'app/core/components/DeleteButton/DeleteButton'; @@ -27,7 +27,7 @@ export class TeamList extends React.Component { this.props.teams.loadTeams(); } - deleteTeam(team: ITeam) { + deleteTeam(team: Team) { this.props.backendSrv.delete('/api/teams/' + team.id).then(this.fetchTeams.bind(this)); } @@ -35,7 +35,7 @@ export class TeamList extends React.Component { this.props.teams.setSearchQuery(evt.target.value); }; - renderTeamMember(team: ITeam): JSX.Element { + renderTeamMember(team: Team): JSX.Element { let teamUrl = `org/teams/edit/${team.id}`; return ( diff --git a/public/app/containers/Teams/TeamMembers.tsx b/public/app/containers/Teams/TeamMembers.tsx index a6b0b04f19d..b06a547063a 100644 --- a/public/app/containers/Teams/TeamMembers.tsx +++ b/public/app/containers/Teams/TeamMembers.tsx @@ -1,13 +1,13 @@ import React from 'react'; import { hot } from 'react-hot-loader'; import { observer } from 'mobx-react'; -import { ITeam, ITeamMember } from 'app/stores/TeamsStore/TeamsStore'; +import { Team, TeamMember } from 'app/stores/TeamsStore/TeamsStore'; import SlideDown from 'app/core/components/Animations/SlideDown'; import { UserPicker, User } from 'app/core/components/Picker/UserPicker'; import DeleteButton from 'app/core/components/DeleteButton/DeleteButton'; interface Props { - team: ITeam; + team: Team; } interface State { @@ -30,15 +30,15 @@ export class TeamMembers extends React.Component { this.props.team.setSearchQuery(evt.target.value); }; - removeMember(member: ITeamMember) { + removeMember(member: TeamMember) { this.props.team.removeMember(member); } - removeMemberConfirmed(member: ITeamMember) { + removeMemberConfirmed(member: TeamMember) { this.props.team.removeMember(member); } - renderMember(member: ITeamMember) { + renderMember(member: TeamMember) { return ( - - - - ); -} - -export default hot(module)(ServerStats); diff --git a/public/app/core/actions/index.ts b/public/app/core/actions/index.ts new file mode 100644 index 00000000000..3c23dbbbe54 --- /dev/null +++ b/public/app/core/actions/index.ts @@ -0,0 +1,3 @@ +import { initNav } from './navModel'; + +export { initNav }; diff --git a/public/app/core/actions/navModel.ts b/public/app/core/actions/navModel.ts new file mode 100644 index 00000000000..048afd4f8ff --- /dev/null +++ b/public/app/core/actions/navModel.ts @@ -0,0 +1,11 @@ +export type Action = InitNavModelAction; + +export interface InitNavModelAction { + type: 'INIT_NAV_MODEL'; + args: string[]; +} + +export const initNav = (...args: string[]): InitNavModelAction => ({ + type: 'INIT_NAV_MODEL', + args: args, +}); diff --git a/public/app/core/components/PageHeader/PageHeader.tsx b/public/app/core/components/PageHeader/PageHeader.tsx index b7bef2495bb..9feddde68ce 100644 --- a/public/app/core/components/PageHeader/PageHeader.tsx +++ b/public/app/core/components/PageHeader/PageHeader.tsx @@ -1,6 +1,6 @@ import React from 'react'; import { observer } from 'mobx-react'; -import { NavModel, NavModelItem } from '../../nav_model_srv'; +import { NavModel, NavModelItem } from 'app/types'; import classNames from 'classnames'; import appEvents from 'app/core/app_events'; import { toJS } from 'mobx'; diff --git a/public/app/core/components/grafana_app.ts b/public/app/core/components/grafana_app.ts index c1cd0e2b5f2..085f0db0a6d 100644 --- a/public/app/core/components/grafana_app.ts +++ b/public/app/core/components/grafana_app.ts @@ -10,7 +10,7 @@ import { createStore } from 'app/stores/store'; import colors from 'app/core/utils/colors'; import { BackendSrv, setBackendSrv } from 'app/core/services/backend_srv'; import { DatasourceSrv } from 'app/features/plugins/datasource_srv'; -import { configureStore } from 'app/store/configureStore'; +import { configureStore } from 'app/stores/configureStore'; export class GrafanaCtrl { /** @ngInject */ diff --git a/public/app/core/reducers/index.ts b/public/app/core/reducers/index.ts new file mode 100644 index 00000000000..0779111c16e --- /dev/null +++ b/public/app/core/reducers/index.ts @@ -0,0 +1,5 @@ +import navModel from './navModel'; + +export default { + navModel, +}; diff --git a/public/app/core/reducers/navModel.ts b/public/app/core/reducers/navModel.ts new file mode 100644 index 00000000000..c00441c4881 --- /dev/null +++ b/public/app/core/reducers/navModel.ts @@ -0,0 +1,64 @@ +import { Action } from 'app/core/actions/navModel'; +import { NavModel, NavModelItem } from 'app/types'; +import config from 'app/core/config'; + +function getNotFoundModel(): NavModel { + var node: NavModelItem = { + id: 'not-found', + text: 'Page not found', + icon: 'fa fa-fw fa-warning', + subTitle: '404 Error', + url: 'not-found', + }; + + return { + breadcrumbs: [node], + node: node, + main: node, + }; +} + +export const initialState: NavModel = getNotFoundModel(); + +const navModelReducer = (state = initialState, action: Action): NavModel => { + switch (action.type) { + case 'INIT_NAV_MODEL': { + let children = config.bootData.navTree as NavModelItem[]; + let main, node; + const parents = []; + + for (const id of action.args) { + node = children.find(el => el.id === id); + + if (!node) { + throw new Error(`NavItem with id ${id} not found`); + } + + children = node.children; + parents.push(node); + } + + main = parents[parents.length - 2]; + + if (main.children) { + for (const item of main.children) { + item.active = false; + + if (item.url === node.url) { + item.active = true; + } + } + } + + return { + main: main, + node: node, + breadcrumbs: [], + }; + } + } + + return state; +}; + +export default navModelReducer; diff --git a/public/app/containers/ServerStats/ServerStats.test.tsx b/public/app/features/server-stats/ServerStats.test.tsx similarity index 100% rename from public/app/containers/ServerStats/ServerStats.test.tsx rename to public/app/features/server-stats/ServerStats.test.tsx diff --git a/public/app/features/server-stats/ServerStats.tsx b/public/app/features/server-stats/ServerStats.tsx new file mode 100644 index 00000000000..b499fb725a8 --- /dev/null +++ b/public/app/features/server-stats/ServerStats.tsx @@ -0,0 +1,69 @@ +import React from 'react'; +import { hot } from 'react-hot-loader'; +import { connect } from 'react-redux'; +import { initNav } from 'app/core/actions'; +import { ContainerProps } from 'app/types'; +import PageHeader from 'app/core/components/PageHeader/PageHeader'; + +interface Props extends ContainerProps {} + +export class ServerStats extends React.Component { + constructor(props) { + super(props); + + this.props.initNav('cfg', 'admin', 'server-stats'); + // const { nav, serverStats } = this.props; + // + // nav.load('cfg', 'admin', 'server-stats'); + // serverStats.load(); + // + // store.dispatch(setNav('new', { asd: 'tasd' })); + } + + render() { + const { navModel } = this.props; + console.log('render', navModel); + return ( +
    + +

    aasd

    +
    + ); + // const { nav, serverStats } = this.props; + // return ( + //
    + // + //
    + //
    +
    {{col.title}} diff --git a/public/app/plugins/panel/table/renderer.ts b/public/app/plugins/panel/table/renderer.ts index 95f54a64904..d85c20a87cc 100644 --- a/public/app/plugins/panel/table/renderer.ts +++ b/public/app/plugins/panel/table/renderer.ts @@ -238,6 +238,10 @@ export class TableRenderer { column.hidden = false; } + if (column.hidden === true) { + return ''; + } + if (column.style && column.style.preserveFormat) { cellClasses.push('table-panel-cell-pre'); } From e37931b79dc07ea19df5ab2891c2588910a22f2d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Tue, 14 Aug 2018 08:52:30 +0200 Subject: [PATCH 407/786] Update CHANGELOG.md --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index efc7e44d31b..f75458820b1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -36,6 +36,8 @@ * **Cloudwatch**: Add new Redshift metrics and dimensions [#12063](https://github.com/grafana/grafana/pulls/12063), thx [@A21z](https://github.com/A21z) * **Table**: Adjust header contrast for the light theme [#12668](https://github.com/grafana/grafana/issues/12668) * **Table**: Fix link color when using light theme and thresholds in use [#12766](https://github.com/grafana/grafana/issues/12766) +om/grafana/grafana/issues/12668) +* **Table**: Fix for useless horizontal scrollbar for table panel [#9964](https://github.com/grafana/grafana/issues/9964) * **Elasticsearch**: For alerting/backend, support having index name to the right of pattern in index pattern [#12731](https://github.com/grafana/grafana/issues/12731) * **OAuth**: Fix overriding tls_skip_verify_insecure using environment variable [#12747](https://github.com/grafana/grafana/issues/12747), thx [@jangaraj](https://github.com/jangaraj) * **Units**: Change units to include characters for power of 2 and 3 [#12744](https://github.com/grafana/grafana/pull/12744), thx [@Worty](https://github.com/Worty) From 7e0482e78d0b71872a1afed3154770922142d991 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Tue, 14 Aug 2018 08:52:51 +0200 Subject: [PATCH 408/786] Fix for Graphite function parameter quoting (#12907) * fix: graphite function parameters should never be quoted for boolean, node, int and float types, fixes #11927 * Update gfunc.ts --- .../app/plugins/datasource/graphite/gfunc.ts | 9 ++++----- .../datasource/graphite/specs/gfunc.jest.ts | 18 ++++++++++++++++++ 2 files changed, 22 insertions(+), 5 deletions(-) diff --git a/public/app/plugins/datasource/graphite/gfunc.ts b/public/app/plugins/datasource/graphite/gfunc.ts index 3d33d0f1005..430d0257b71 100644 --- a/public/app/plugins/datasource/graphite/gfunc.ts +++ b/public/app/plugins/datasource/graphite/gfunc.ts @@ -973,13 +973,12 @@ export class FuncInstance { } else if (_.get(_.last(this.def.params), 'multiple')) { paramType = _.get(_.last(this.def.params), 'type'); } - if (paramType === 'value_or_series') { + // param types that should never be quoted + if (_.includes(['value_or_series', 'boolean', 'int', 'float', 'node'], paramType)) { return value; } - if (paramType === 'boolean' && _.includes(['true', 'false'], value)) { - return value; - } - if (_.includes(['int', 'float', 'int_or_interval', 'node_or_tag', 'node'], paramType) && _.isFinite(+value)) { + // param types that might be quoted + if (_.includes(['int_or_interval', 'node_or_tag'], paramType) && _.isFinite(+value)) { return _.toString(+value); } return "'" + value + "'"; diff --git a/public/app/plugins/datasource/graphite/specs/gfunc.jest.ts b/public/app/plugins/datasource/graphite/specs/gfunc.jest.ts index feeaea2df67..08373582e73 100644 --- a/public/app/plugins/datasource/graphite/specs/gfunc.jest.ts +++ b/public/app/plugins/datasource/graphite/specs/gfunc.jest.ts @@ -55,6 +55,24 @@ describe('when rendering func instance', function() { expect(func.render('hello')).toEqual("movingMedian(hello, '5min')"); }); + it('should never quote boolean paramater', function() { + var func = gfunc.createFuncInstance('sortByName'); + func.params[0] = '$natural'; + expect(func.render('hello')).toEqual('sortByName(hello, $natural)'); + }); + + it('should never quote int paramater', function() { + var func = gfunc.createFuncInstance('maximumAbove'); + func.params[0] = '$value'; + expect(func.render('hello')).toEqual('maximumAbove(hello, $value)'); + }); + + it('should never quote node paramater', function() { + var func = gfunc.createFuncInstance('aliasByNode'); + func.params[0] = '$node'; + expect(func.render('hello')).toEqual('aliasByNode(hello, $node)'); + }); + it('should handle metric param and int param and string param', function() { var func = gfunc.createFuncInstance('groupByNode'); func.params[0] = 5; From 0fa47c5ef49ba645e6945fe2d5004e84a36a5563 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Tue, 14 Aug 2018 08:55:27 +0200 Subject: [PATCH 409/786] Update CHANGELOG.md --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f75458820b1..8af8027508a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,7 +13,6 @@ * **Api**: Delete nonexistent datasource should return 404 [#12313](https://github.com/grafana/grafana/issues/12313), thx [@AustinWinstanley](https://github.com/AustinWinstanley) * **Dashboard**: Fix selecting current dashboard from search should not reload dashboard [#12248](https://github.com/grafana/grafana/issues/12248) * **Singlestat**: Make colorization of prefix and postfix optional in singlestat [#11892](https://github.com/grafana/grafana/pull/11892), thx [@ApsOps](https://github.com/ApsOps) -* **Table**: Make table sorting stable when null values exist [#12362](https://github.com/grafana/grafana/pull/12362), thx [@bz2](https://github.com/bz2) * **Prometheus**: Fix graph panel bar width issue in aligned prometheus queries [#12379](https://github.com/grafana/grafana/issues/12379) * **Prometheus**: Heatmap - fix unhandled error when some points are missing [#12484](https://github.com/grafana/grafana/issues/12484) * **Prometheus**: Add $__interval, $__interval_ms, $__range, $__range_s & $__range_ms support for dashboard and template queries [#12597](https://github.com/grafana/grafana/issues/12597) [#12882](https://github.com/grafana/grafana/issues/12882), thx [@roidelapluie](https://github.com/roidelapluie) @@ -38,6 +37,7 @@ * **Table**: Fix link color when using light theme and thresholds in use [#12766](https://github.com/grafana/grafana/issues/12766) om/grafana/grafana/issues/12668) * **Table**: Fix for useless horizontal scrollbar for table panel [#9964](https://github.com/grafana/grafana/issues/9964) +* **Table**: Make table sorting stable when null values exist [#12362](https://github.com/grafana/grafana/pull/12362), thx [@bz2](https://github.com/bz2) * **Elasticsearch**: For alerting/backend, support having index name to the right of pattern in index pattern [#12731](https://github.com/grafana/grafana/issues/12731) * **OAuth**: Fix overriding tls_skip_verify_insecure using environment variable [#12747](https://github.com/grafana/grafana/issues/12747), thx [@jangaraj](https://github.com/jangaraj) * **Units**: Change units to include characters for power of 2 and 3 [#12744](https://github.com/grafana/grafana/pull/12744), thx [@Worty](https://github.com/Worty) @@ -46,6 +46,7 @@ om/grafana/grafana/issues/12668) * **Datasource**: Fix UI issue with secret fields after updating datasource [#11270](https://github.com/grafana/grafana/issues/11270) * **Plugins**: Convert URL-like text to links in plugins readme [#12843](https://github.com/grafana/grafana/pull/12843), thx [pgiraud](https://github.com/pgiraud) * **Docker**: Make it possible to set a specific plugin url [#12861](https://github.com/grafana/grafana/pull/12861), thx [ClementGautier](https://github.com/ClementGautier) +* **Graphite**: Fix for quoting of int function parameters (when using variables) [#11927](https://github.com/grafana/grafana/pull/11927) ### Breaking changes From 53bab1a84bfb38e21762bdd40cdb70ca48994f4b Mon Sep 17 00:00:00 2001 From: Tobias Skarhed Date: Tue, 14 Aug 2018 09:15:14 +0200 Subject: [PATCH 410/786] Remove tests and logs --- .../panel/heatmap/HeatmapRenderContainer.tsx | 20 - public/app/plugins/panel/heatmap/rendering.ts | 3 +- .../panel/heatmap/specs/renderer.jest.ts | 351 ------------------ .../panel/heatmap/specs/renderer_specs.ts | 320 ---------------- 4 files changed, 1 insertion(+), 693 deletions(-) delete mode 100644 public/app/plugins/panel/heatmap/HeatmapRenderContainer.tsx delete mode 100644 public/app/plugins/panel/heatmap/specs/renderer.jest.ts delete mode 100644 public/app/plugins/panel/heatmap/specs/renderer_specs.ts diff --git a/public/app/plugins/panel/heatmap/HeatmapRenderContainer.tsx b/public/app/plugins/panel/heatmap/HeatmapRenderContainer.tsx deleted file mode 100644 index e5982a485ca..00000000000 --- a/public/app/plugins/panel/heatmap/HeatmapRenderContainer.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import React from 'react'; -import HeatmapRenderer from './rendering'; -import { HeatmapCtrl } from './heatmap_ctrl'; - -export class HeatmapRenderContainer extends React.Component { - renderer: any; - constructor(props) { - super(props); - this.renderer = HeatmapRenderer( - this.props.scope, - this.props.children[0], - [], - new HeatmapCtrl(this.props.scope, {}, {}) - ); - } - - render() { - return
    ; - } -} diff --git a/public/app/plugins/panel/heatmap/rendering.ts b/public/app/plugins/panel/heatmap/rendering.ts index 6d3d21420e0..e3318ea7e23 100644 --- a/public/app/plugins/panel/heatmap/rendering.ts +++ b/public/app/plugins/panel/heatmap/rendering.ts @@ -154,7 +154,7 @@ export class HeatmapRenderer { } else { timeFormat = d3.timeFormat(grafanaTimeFormatter); } - console.log(ticks); + let xAxis = d3 .axisBottom(this.xScale) .ticks(ticks) @@ -549,7 +549,6 @@ export class HeatmapRenderer { .style('opacity', this.getCardOpacity.bind(this)); let $cards = this.$heatmap.find('.heatmap-card'); - console.log($cards); $cards .on('mouseenter', event => { this.tooltip.mouseOverBucket = true; diff --git a/public/app/plugins/panel/heatmap/specs/renderer.jest.ts b/public/app/plugins/panel/heatmap/specs/renderer.jest.ts deleted file mode 100644 index a5546624d65..00000000000 --- a/public/app/plugins/panel/heatmap/specs/renderer.jest.ts +++ /dev/null @@ -1,351 +0,0 @@ -// import { describe, beforeEach, it, sinon, expect, angularMocks } from '../../../../../test/lib/common'; - -import '../module'; -// import angular from 'angular'; -// import $ from 'jquery'; -// import helpers from 'test/specs/helpers'; -import TimeSeries from 'app/core/time_series2'; -import moment from 'moment'; -// import { Emitter } from 'app/core/core'; -import rendering from '../rendering'; -// import * as d3 from 'd3'; -import { convertToHeatMap, convertToCards, histogramToHeatmap, calculateBucketSize } from '../heatmap_data_converter'; -jest.mock('app/core/core', () => ({ - appEvents: { - on: () => {}, - }, - contextSrv: { - user: { - lightTheme: false, - }, - }, -})); - -describe('grafanaHeatmap', function() { - // beforeEach(angularMocks.module('grafana.core')); - - let scope = {}; - let render; - - function heatmapScenario(desc, func, elementWidth = 500) { - describe(desc, function() { - var ctx: any = {}; - - ctx.setup = function(setupFunc) { - // beforeEach( - // angularMocks.module(function($provide) { - // $provide.value('timeSrv', new helpers.TimeSrvStub()); - // }) - // ); - - beforeEach(() => { - // angularMocks.inject(function($rootScope, $compile) { - var ctrl: any = { - colorSchemes: [ - { - name: 'Oranges', - value: 'interpolateOranges', - invert: 'dark', - }, - { name: 'Reds', value: 'interpolateReds', invert: 'dark' }, - ], - events: { - on: () => {}, - emit: () => {}, - }, - height: 200, - panel: { - heatmap: {}, - cards: { - cardPadding: null, - cardRound: null, - }, - color: { - mode: 'spectrum', - cardColor: '#b4ff00', - colorScale: 'linear', - exponent: 0.5, - colorScheme: 'interpolateOranges', - fillBackground: false, - }, - legend: { - show: false, - }, - xBucketSize: 1000, - xBucketNumber: null, - yBucketSize: 1, - yBucketNumber: null, - xAxis: { - show: true, - }, - yAxis: { - show: true, - format: 'short', - decimals: null, - logBase: 1, - splitFactor: null, - min: null, - max: null, - removeZeroValues: false, - }, - tooltip: { - show: true, - seriesStat: false, - showHistogram: false, - }, - highlightCards: true, - }, - renderingCompleted: jest.fn(), - hiddenSeries: {}, - dashboard: { - getTimezone: () => 'utc', - }, - range: { - from: moment.utc('01 Mar 2017 10:00:00', 'DD MMM YYYY HH:mm:ss'), - to: moment.utc('01 Mar 2017 11:00:00', 'DD MMM YYYY HH:mm:ss'), - }, - }; - - // var scope = $rootScope.$new(); - scope.ctrl = ctrl; - - ctx.series = []; - ctx.series.push( - new TimeSeries({ - datapoints: [[1, 1422774000000], [2, 1422774060000]], - alias: 'series1', - }) - ); - ctx.series.push( - new TimeSeries({ - datapoints: [[2, 1422774000000], [3, 1422774060000]], - alias: 'series2', - }) - ); - - ctx.data = { - heatmapStats: { - min: 1, - max: 3, - minLog: 1, - }, - xBucketSize: ctrl.panel.xBucketSize, - yBucketSize: ctrl.panel.yBucketSize, - }; - - setupFunc(ctrl, ctx); - - let logBase = ctrl.panel.yAxis.logBase; - let bucketsData; - if (ctrl.panel.dataFormat === 'tsbuckets') { - bucketsData = histogramToHeatmap(ctx.series); - } else { - bucketsData = convertToHeatMap(ctx.series, ctx.data.yBucketSize, ctx.data.xBucketSize, logBase); - } - ctx.data.buckets = bucketsData; - - let { cards, cardStats } = convertToCards(bucketsData); - ctx.data.cards = cards; - ctx.data.cardStats = cardStats; - - // let elemHtml = ` - //
    - //
    - //
    - //
    - //
    `; - - // var element = $.parseHTML(elemHtml); - // $compile(element)(scope); - // scope.$digest(); - - ctrl.data = ctx.data; - ctx.element = { - find: () => ({ - on: () => {}, - css: () => 189, - width: () => 189, - height: () => 200, - find: () => ({ - on: () => {}, - }), - }), - on: () => {}, - }; - render = rendering(scope, ctx.element, [], ctrl); - render.render(); - render.ctrl.renderingCompleted(); - }); - }; - - func(ctx); - }); - } - - heatmapScenario('default options', function(ctx) { - ctx.setup(function(ctrl) { - ctrl.panel.yAxis.logBase = 1; - }); - - it('should draw correct Y axis', function() { - console.log('Runnign first test'); - // console.log(render.ctrl.data); - console.log(render.scope.yScale); - var yTicks = getTicks(ctx.element, '.axis-y'); - expect(yTicks).toEqual(['1', '2', '3']); - }); - - it('should draw correct X axis', function() { - var xTicks = getTicks(ctx.element, '.axis-x'); - let expectedTicks = [ - formatTime('01 Mar 2017 10:00:00'), - formatTime('01 Mar 2017 10:15:00'), - formatTime('01 Mar 2017 10:30:00'), - formatTime('01 Mar 2017 10:45:00'), - formatTime('01 Mar 2017 11:00:00'), - ]; - expect(xTicks).toEqual(expectedTicks); - }); - }); - - heatmapScenario('when logBase is 2', function(ctx) { - ctx.setup(function(ctrl) { - ctrl.panel.yAxis.logBase = 2; - }); - - it('should draw correct Y axis', function() { - var yTicks = getTicks(ctx.element, '.axis-y'); - expect(yTicks).toEqual(['1', '2', '4']); - }); - }); - - heatmapScenario('when logBase is 10', function(ctx) { - ctx.setup(function(ctrl, ctx) { - ctrl.panel.yAxis.logBase = 10; - - ctx.series.push( - new TimeSeries({ - datapoints: [[10, 1422774000000], [20, 1422774060000]], - alias: 'series3', - }) - ); - ctx.data.heatmapStats.max = 20; - }); - - it('should draw correct Y axis', function() { - var yTicks = getTicks(ctx.element, '.axis-y'); - expect(yTicks).toEqual(['1', '10', '100']); - }); - }); - - heatmapScenario('when logBase is 32', function(ctx) { - ctx.setup(function(ctrl) { - ctrl.panel.yAxis.logBase = 32; - - ctx.series.push( - new TimeSeries({ - datapoints: [[10, 1422774000000], [100, 1422774060000]], - alias: 'series3', - }) - ); - ctx.data.heatmapStats.max = 100; - }); - - it('should draw correct Y axis', function() { - var yTicks = getTicks(ctx.element, '.axis-y'); - expect(yTicks).toEqual(['1', '32', '1.0 K']); - }); - }); - - heatmapScenario('when logBase is 1024', function(ctx) { - ctx.setup(function(ctrl) { - ctrl.panel.yAxis.logBase = 1024; - - ctx.series.push( - new TimeSeries({ - datapoints: [[2000, 1422774000000], [300000, 1422774060000]], - alias: 'series3', - }) - ); - ctx.data.heatmapStats.max = 300000; - }); - - it('should draw correct Y axis', function() { - var yTicks = getTicks(ctx.element, '.axis-y'); - expect(yTicks).toEqual(['1', '1 K', '1.0 Mil']); - }); - }); - - heatmapScenario('when Y axis format set to "none"', function(ctx) { - ctx.setup(function(ctrl) { - ctrl.panel.yAxis.logBase = 1; - ctrl.panel.yAxis.format = 'none'; - ctx.data.heatmapStats.max = 10000; - }); - - it('should draw correct Y axis', function() { - var yTicks = getTicks(ctx.element, '.axis-y'); - expect(yTicks).toEqual(['0', '2000', '4000', '6000', '8000', '10000', '12000']); - }); - }); - - heatmapScenario('when Y axis format set to "second"', function(ctx) { - ctx.setup(function(ctrl) { - ctrl.panel.yAxis.logBase = 1; - ctrl.panel.yAxis.format = 's'; - ctx.data.heatmapStats.max = 3600; - }); - - it('should draw correct Y axis', function() { - var yTicks = getTicks(ctx.element, '.axis-y'); - expect(yTicks).toEqual(['0 ns', '17 min', '33 min', '50 min', '1.11 hour']); - }); - }); - - heatmapScenario('when data format is Time series buckets', function(ctx) { - ctx.setup(function(ctrl, ctx) { - ctrl.panel.dataFormat = 'tsbuckets'; - - const series = [ - { - alias: '1', - datapoints: [[1000, 1422774000000], [200000, 1422774060000]], - }, - { - alias: '2', - datapoints: [[3000, 1422774000000], [400000, 1422774060000]], - }, - { - alias: '3', - datapoints: [[2000, 1422774000000], [300000, 1422774060000]], - }, - ]; - ctx.series = series.map(s => new TimeSeries(s)); - - ctx.data.tsBuckets = series.map(s => s.alias).concat(''); - ctx.data.yBucketSize = 1; - let xBucketBoundSet = series[0].datapoints.map(dp => dp[1]); - ctx.data.xBucketSize = calculateBucketSize(xBucketBoundSet); - }); - - it('should draw correct Y axis', function() { - var yTicks = getTicks(ctx.element, '.axis-y'); - expect(yTicks).toEqual(['1', '2', '3', '']); - }); - }); -}); - -function getTicks(element, axisSelector) { - // return element - // .find(axisSelector) - // .find('text') - // .map(function() { - // return this.textContent; - // }) - // .get(); -} - -function formatTime(timeStr) { - let format = 'HH:mm'; - return moment.utc(timeStr, 'DD MMM YYYY HH:mm:ss').format(format); -} diff --git a/public/app/plugins/panel/heatmap/specs/renderer_specs.ts b/public/app/plugins/panel/heatmap/specs/renderer_specs.ts deleted file mode 100644 index f52b6d1d985..00000000000 --- a/public/app/plugins/panel/heatmap/specs/renderer_specs.ts +++ /dev/null @@ -1,320 +0,0 @@ -import { describe, beforeEach, it, sinon, expect, angularMocks } from '../../../../../test/lib/common'; - -import '../module'; -import angular from 'angular'; -import $ from 'jquery'; -import helpers from 'test/specs/helpers'; -import TimeSeries from 'app/core/time_series2'; -import moment from 'moment'; -import { Emitter } from 'app/core/core'; -import rendering from '../rendering'; -import { convertToHeatMap, convertToCards, histogramToHeatmap, calculateBucketSize } from '../heatmap_data_converter'; - -describe('grafanaHeatmap', function() { - beforeEach(angularMocks.module('grafana.core')); - - function heatmapScenario(desc, func, elementWidth = 500) { - describe(desc, function() { - var ctx: any = {}; - - ctx.setup = function(setupFunc) { - beforeEach( - angularMocks.module(function($provide) { - $provide.value('timeSrv', new helpers.TimeSrvStub()); - }) - ); - - beforeEach( - angularMocks.inject(function($rootScope, $compile) { - var ctrl: any = { - colorSchemes: [ - { - name: 'Oranges', - value: 'interpolateOranges', - invert: 'dark', - }, - { name: 'Reds', value: 'interpolateReds', invert: 'dark' }, - ], - events: new Emitter(), - height: 200, - panel: { - heatmap: {}, - cards: { - cardPadding: null, - cardRound: null, - }, - color: { - mode: 'spectrum', - cardColor: '#b4ff00', - colorScale: 'linear', - exponent: 0.5, - colorScheme: 'interpolateOranges', - fillBackground: false, - }, - legend: { - show: false, - }, - xBucketSize: 1000, - xBucketNumber: null, - yBucketSize: 1, - yBucketNumber: null, - xAxis: { - show: true, - }, - yAxis: { - show: true, - format: 'short', - decimals: null, - logBase: 1, - splitFactor: null, - min: null, - max: null, - removeZeroValues: false, - }, - tooltip: { - show: true, - seriesStat: false, - showHistogram: false, - }, - highlightCards: true, - }, - renderingCompleted: sinon.spy(), - hiddenSeries: {}, - dashboard: { - getTimezone: sinon.stub().returns('utc'), - }, - range: { - from: moment.utc('01 Mar 2017 10:00:00', 'DD MMM YYYY HH:mm:ss'), - to: moment.utc('01 Mar 2017 11:00:00', 'DD MMM YYYY HH:mm:ss'), - }, - }; - - var scope = $rootScope.$new(); - scope.ctrl = ctrl; - - ctx.series = []; - ctx.series.push( - new TimeSeries({ - datapoints: [[1, 1422774000000], [2, 1422774060000]], - alias: 'series1', - }) - ); - ctx.series.push( - new TimeSeries({ - datapoints: [[2, 1422774000000], [3, 1422774060000]], - alias: 'series2', - }) - ); - - ctx.data = { - heatmapStats: { - min: 1, - max: 3, - minLog: 1, - }, - xBucketSize: ctrl.panel.xBucketSize, - yBucketSize: ctrl.panel.yBucketSize, - }; - - setupFunc(ctrl, ctx); - - let logBase = ctrl.panel.yAxis.logBase; - let bucketsData; - if (ctrl.panel.dataFormat === 'tsbuckets') { - bucketsData = histogramToHeatmap(ctx.series); - } else { - bucketsData = convertToHeatMap(ctx.series, ctx.data.yBucketSize, ctx.data.xBucketSize, logBase); - } - ctx.data.buckets = bucketsData; - - let { cards, cardStats } = convertToCards(bucketsData); - ctx.data.cards = cards; - ctx.data.cardStats = cardStats; - - let elemHtml = ` -
    -
    -
    -
    -
    `; - - var element = angular.element(elemHtml); - $compile(element)(scope); - scope.$digest(); - - ctrl.data = ctx.data; - ctx.element = element; - rendering(scope, $(element), [], ctrl); - ctrl.events.emit('render'); - }) - ); - }; - - func(ctx); - }); - } - - heatmapScenario('default options', function(ctx) { - ctx.setup(function(ctrl) { - ctrl.panel.yAxis.logBase = 1; - }); - - it('should draw correct Y axis', function() { - var yTicks = getTicks(ctx.element, '.axis-y'); - expect(yTicks).to.eql(['1', '2', '3']); - }); - - it('should draw correct X axis', function() { - var xTicks = getTicks(ctx.element, '.axis-x'); - let expectedTicks = [ - formatTime('01 Mar 2017 10:00:00'), - formatTime('01 Mar 2017 10:15:00'), - formatTime('01 Mar 2017 10:30:00'), - formatTime('01 Mar 2017 10:45:00'), - formatTime('01 Mar 2017 11:00:00'), - ]; - expect(xTicks).to.eql(expectedTicks); - }); - }); - - heatmapScenario('when logBase is 2', function(ctx) { - ctx.setup(function(ctrl) { - ctrl.panel.yAxis.logBase = 2; - }); - - it('should draw correct Y axis', function() { - var yTicks = getTicks(ctx.element, '.axis-y'); - expect(yTicks).to.eql(['1', '2', '4']); - }); - }); - - heatmapScenario('when logBase is 10', function(ctx) { - ctx.setup(function(ctrl, ctx) { - ctrl.panel.yAxis.logBase = 10; - - ctx.series.push( - new TimeSeries({ - datapoints: [[10, 1422774000000], [20, 1422774060000]], - alias: 'series3', - }) - ); - ctx.data.heatmapStats.max = 20; - }); - - it('should draw correct Y axis', function() { - var yTicks = getTicks(ctx.element, '.axis-y'); - expect(yTicks).to.eql(['1', '10', '100']); - }); - }); - - heatmapScenario('when logBase is 32', function(ctx) { - ctx.setup(function(ctrl) { - ctrl.panel.yAxis.logBase = 32; - - ctx.series.push( - new TimeSeries({ - datapoints: [[10, 1422774000000], [100, 1422774060000]], - alias: 'series3', - }) - ); - ctx.data.heatmapStats.max = 100; - }); - - it('should draw correct Y axis', function() { - var yTicks = getTicks(ctx.element, '.axis-y'); - expect(yTicks).to.eql(['1', '32', '1.0 K']); - }); - }); - - heatmapScenario('when logBase is 1024', function(ctx) { - ctx.setup(function(ctrl) { - ctrl.panel.yAxis.logBase = 1024; - - ctx.series.push( - new TimeSeries({ - datapoints: [[2000, 1422774000000], [300000, 1422774060000]], - alias: 'series3', - }) - ); - ctx.data.heatmapStats.max = 300000; - }); - - it('should draw correct Y axis', function() { - var yTicks = getTicks(ctx.element, '.axis-y'); - expect(yTicks).to.eql(['1', '1 K', '1.0 Mil']); - }); - }); - - heatmapScenario('when Y axis format set to "none"', function(ctx) { - ctx.setup(function(ctrl) { - ctrl.panel.yAxis.logBase = 1; - ctrl.panel.yAxis.format = 'none'; - ctx.data.heatmapStats.max = 10000; - }); - - it('should draw correct Y axis', function() { - var yTicks = getTicks(ctx.element, '.axis-y'); - expect(yTicks).to.eql(['0', '2000', '4000', '6000', '8000', '10000', '12000']); - }); - }); - - heatmapScenario('when Y axis format set to "second"', function(ctx) { - ctx.setup(function(ctrl) { - ctrl.panel.yAxis.logBase = 1; - ctrl.panel.yAxis.format = 's'; - ctx.data.heatmapStats.max = 3600; - }); - - it('should draw correct Y axis', function() { - var yTicks = getTicks(ctx.element, '.axis-y'); - expect(yTicks).to.eql(['0 ns', '17 min', '33 min', '50 min', '1.11 hour']); - }); - }); - - heatmapScenario('when data format is Time series buckets', function(ctx) { - ctx.setup(function(ctrl, ctx) { - ctrl.panel.dataFormat = 'tsbuckets'; - - const series = [ - { - alias: '1', - datapoints: [[1000, 1422774000000], [200000, 1422774060000]], - }, - { - alias: '2', - datapoints: [[3000, 1422774000000], [400000, 1422774060000]], - }, - { - alias: '3', - datapoints: [[2000, 1422774000000], [300000, 1422774060000]], - }, - ]; - ctx.series = series.map(s => new TimeSeries(s)); - - ctx.data.tsBuckets = series.map(s => s.alias).concat(''); - ctx.data.yBucketSize = 1; - let xBucketBoundSet = series[0].datapoints.map(dp => dp[1]); - ctx.data.xBucketSize = calculateBucketSize(xBucketBoundSet); - }); - - it('should draw correct Y axis', function() { - var yTicks = getTicks(ctx.element, '.axis-y'); - expect(yTicks).to.eql(['1', '2', '3', '']); - }); - }); -}); - -function getTicks(element, axisSelector) { - return element - .find(axisSelector) - .find('text') - .map(function() { - return this.textContent; - }) - .get(); -} - -function formatTime(timeStr) { - let format = 'HH:mm'; - return moment.utc(timeStr, 'DD MMM YYYY HH:mm:ss').format(format); -} From 3955133f7e143002bd7b141808a1323ade444694 Mon Sep 17 00:00:00 2001 From: Sven Klemm Date: Tue, 14 Aug 2018 09:15:24 +0200 Subject: [PATCH 411/786] Don't pass datasource to newPostgresMacroEngine --- pkg/tsdb/postgres/macros.go | 7 ++----- pkg/tsdb/postgres/macros_test.go | 9 ++------- pkg/tsdb/postgres/postgres.go | 4 +++- 3 files changed, 7 insertions(+), 13 deletions(-) diff --git a/pkg/tsdb/postgres/macros.go b/pkg/tsdb/postgres/macros.go index 81b0da9fbce..0a9162a2d4c 100644 --- a/pkg/tsdb/postgres/macros.go +++ b/pkg/tsdb/postgres/macros.go @@ -7,7 +7,6 @@ import ( "strings" "time" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/tsdb" ) @@ -21,10 +20,8 @@ type postgresMacroEngine struct { timescaledb bool } -func newPostgresMacroEngine(datasource *models.DataSource) tsdb.SqlMacroEngine { - engine := &postgresMacroEngine{} - engine.timescaledb = datasource.JsonData.Get("timescaledb").MustBool(false) - return engine +func newPostgresMacroEngine(timescaledb bool) tsdb.SqlMacroEngine { + return &postgresMacroEngine{timescaledb: timescaledb} } func (m *postgresMacroEngine) Interpolate(query *tsdb.Query, timeRange *tsdb.TimeRange, sql string) (string, error) { diff --git a/pkg/tsdb/postgres/macros_test.go b/pkg/tsdb/postgres/macros_test.go index fe95535fe0c..30a57a7095f 100644 --- a/pkg/tsdb/postgres/macros_test.go +++ b/pkg/tsdb/postgres/macros_test.go @@ -6,19 +6,14 @@ import ( "testing" "time" - "github.com/grafana/grafana/pkg/components/simplejson" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/tsdb" . "github.com/smartystreets/goconvey/convey" ) func TestMacroEngine(t *testing.T) { Convey("MacroEngine", t, func() { - datasource := &models.DataSource{JsonData: simplejson.New()} - engine := newPostgresMacroEngine(datasource) - datasourceTS := &models.DataSource{JsonData: simplejson.New()} - datasourceTS.JsonData.Set("timescaledb", true) - engineTS := newPostgresMacroEngine(datasourceTS) + engine := newPostgresMacroEngine(false) + engineTS := newPostgresMacroEngine(true) query := &tsdb.Query{} Convey("Given a time range between 2018-04-12 00:00 and 2018-04-12 00:05", func() { diff --git a/pkg/tsdb/postgres/postgres.go b/pkg/tsdb/postgres/postgres.go index 46d766f9a11..4bcf06638f4 100644 --- a/pkg/tsdb/postgres/postgres.go +++ b/pkg/tsdb/postgres/postgres.go @@ -32,7 +32,9 @@ func newPostgresQueryEndpoint(datasource *models.DataSource) (tsdb.TsdbQueryEndp log: logger, } - return tsdb.NewSqlQueryEndpoint(&config, &rowTransformer, newPostgresMacroEngine(datasource), logger) + timescaledb := datasource.JsonData.Get("timescaledb").MustBool(false) + + return tsdb.NewSqlQueryEndpoint(&config, &rowTransformer, newPostgresMacroEngine(timescaledb), logger) } func generateConnectionString(datasource *models.DataSource) string { From 4f704cec532529542dbc8c1912e666e168d4b36e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Tue, 14 Aug 2018 09:18:04 +0200 Subject: [PATCH 412/786] fix: ds_proxy test not initiating header --- pkg/api/pluginproxy/ds_proxy_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/api/pluginproxy/ds_proxy_test.go b/pkg/api/pluginproxy/ds_proxy_test.go index bb553b4d075..9b768c3d32a 100644 --- a/pkg/api/pluginproxy/ds_proxy_test.go +++ b/pkg/api/pluginproxy/ds_proxy_test.go @@ -219,7 +219,7 @@ func TestDSRouteRule(t *testing.T) { proxy := NewDataSourceProxy(ds, plugin, ctx, "/render") requestURL, _ := url.Parse("http://grafana.com/sub") - req := http.Request{URL: requestURL} + req := http.Request{URL: requestURL, Header: http.Header{}} proxy.getDirector()(&req) @@ -244,7 +244,7 @@ func TestDSRouteRule(t *testing.T) { proxy := NewDataSourceProxy(ds, plugin, ctx, "") requestURL, _ := url.Parse("http://grafana.com/sub") - req := http.Request{URL: requestURL} + req := http.Request{URL: requestURL, Header: http.Header{}} proxy.getDirector()(&req) From 766d0bef17fde119ffddbc827177e2c3f4d36fe3 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Tue, 14 Aug 2018 09:19:37 +0200 Subject: [PATCH 413/786] changelog: add notes about closing #10705 [skip ci] --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8af8027508a..7cd75402946 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ * **Api**: Delete nonexistent datasource should return 404 [#12313](https://github.com/grafana/grafana/issues/12313), thx [@AustinWinstanley](https://github.com/AustinWinstanley) * **Dashboard**: Fix selecting current dashboard from search should not reload dashboard [#12248](https://github.com/grafana/grafana/issues/12248) +* **Dashboard**: Use uid when linking to dashboards internally in a dashboard [#10705](https://github.com/grafana/grafana/issues/10705) * **Singlestat**: Make colorization of prefix and postfix optional in singlestat [#11892](https://github.com/grafana/grafana/pull/11892), thx [@ApsOps](https://github.com/ApsOps) * **Prometheus**: Fix graph panel bar width issue in aligned prometheus queries [#12379](https://github.com/grafana/grafana/issues/12379) * **Prometheus**: Heatmap - fix unhandled error when some points are missing [#12484](https://github.com/grafana/grafana/issues/12484) @@ -27,7 +28,6 @@ * **Github OAuth**: Allow changes of user info at Github to be synched to Grafana when signing in [#11818](https://github.com/grafana/grafana/issues/11818), thx [@rwaweber](https://github.com/rwaweber) * **Alerting**: Fix diff and percent_diff reducers [#11563](https://github.com/grafana/grafana/issues/11563), thx [@jessetane](https://github.com/jessetane) * **Alerting**: Fix rendering timeout which could cause notifications to not be sent due to rendering timing out [#12151](https://github.com/grafana/grafana/issues/12151) -* **Units**: Polish złoty currency [#12691](https://github.com/grafana/grafana/pull/12691), thx [@mwegrzynek](https://github.com/mwegrzynek) * **Cloudwatch**: Improved error handling [#12489](https://github.com/grafana/grafana/issues/12489), thx [@mtanda](https://github.com/mtanda) * **Cloudwatch**: AppSync metrics and dimensions [#12300](https://github.com/grafana/grafana/issues/12300), thx [@franciscocpg](https://github.com/franciscocpg) * **Cloudwatch**: Direct Connect metrics and dimensions [#12762](https://github.com/grafana/grafana/pulls/12762), thx [@mindriot88](https://github.com/mindriot88) @@ -41,6 +41,7 @@ om/grafana/grafana/issues/12668) * **Elasticsearch**: For alerting/backend, support having index name to the right of pattern in index pattern [#12731](https://github.com/grafana/grafana/issues/12731) * **OAuth**: Fix overriding tls_skip_verify_insecure using environment variable [#12747](https://github.com/grafana/grafana/issues/12747), thx [@jangaraj](https://github.com/jangaraj) * **Units**: Change units to include characters for power of 2 and 3 [#12744](https://github.com/grafana/grafana/pull/12744), thx [@Worty](https://github.com/Worty) +* **Units**: Polish złoty currency [#12691](https://github.com/grafana/grafana/pull/12691), thx [@mwegrzynek](https://github.com/mwegrzynek) * **Graph**: Option to hide series from tooltip [#3341](https://github.com/grafana/grafana/issues/3341), thx [@mtanda](https://github.com/mtanda) * **UI**: Fix iOS home screen "app" icon and Windows 10 app experience [#12752](https://github.com/grafana/grafana/issues/12752), thx [@andig](https://github.com/andig) * **Datasource**: Fix UI issue with secret fields after updating datasource [#11270](https://github.com/grafana/grafana/issues/11270) From e696dc4d5f895d0c17ff3e02ac2c9181d2b234ab Mon Sep 17 00:00:00 2001 From: Tobias Skarhed Date: Tue, 14 Aug 2018 09:28:08 +0200 Subject: [PATCH 414/786] Remove Karma scripts and docs --- .github/CONTRIBUTING.md | 6 ++++- README.md | 12 ++-------- docs/sources/project/building_from_source.md | 8 +++---- package.json | 10 --------- scripts/grunt/default_task.js | 1 - scripts/grunt/options/karma.js | 23 -------------------- 6 files changed, 10 insertions(+), 50 deletions(-) delete mode 100644 scripts/grunt/options/karma.js diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index fe0a1d6c548..f0f4e19bfc3 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -7,7 +7,11 @@ grunt && grunt watch ### Rerun tests on source change ``` -grunt karma:dev +npm jest +``` +or +``` +yarn jest ``` ### Run tests for backend assets before commit diff --git a/README.md b/README.md index d6083bb1504..71fdb04cea6 100644 --- a/README.md +++ b/README.md @@ -59,11 +59,6 @@ Run tests yarn run jest ``` -Run karma tests -```bash -yarn run karma -``` - ### Recompile backend on source change To rebuild on source change. @@ -101,14 +96,11 @@ Execute all frontend tests yarn run test ``` -Writing & watching frontend tests (we have two test runners) +Writing & watching frontend tests - jest for all new tests that do not require browser context (React+more) - Start watcher: `yarn run jest` - - Jest will run all test files that end with the name ".jest.ts" -- karma + mocha is used for testing angularjs components. We do want to migrate these test to jest over time (if possible). - - Start watcher: `yarn run karma` - - Karma+Mocha runs all files that end with the name "_specs.ts". + - Jest will run all test files that end with the name ".test.ts" #### Backend ```bash diff --git a/docs/sources/project/building_from_source.md b/docs/sources/project/building_from_source.md index a0b553594ce..20c177211e3 100644 --- a/docs/sources/project/building_from_source.md +++ b/docs/sources/project/building_from_source.md @@ -90,14 +90,12 @@ You'll also need to run `npm run watch` to watch for changes to the front-end (t - You can run backend Golang tests using "go test ./pkg/...". - Execute all frontend tests with "npm run test" -Writing & watching frontend tests (we have two test runners) +Writing & watching frontend tests - jest for all new tests that do not require browser context (React+more) - Start watcher: `npm run jest` - - Jest will run all test files that end with the name ".jest.ts" -- karma + mocha is used for testing angularjs components. We do want to migrate these test to jest over time (if possible). - - Start watcher: `npm run karma` - - Karma+Mocha runs all files that end with the name "_specs.ts". + - Jest will run all test files that end with the name ".test.ts" + ## Creating optimized release packages diff --git a/package.json b/package.json index 24e23b574df..87615e8273b 100644 --- a/package.json +++ b/package.json @@ -46,7 +46,6 @@ "grunt-contrib-copy": "~1.0.0", "grunt-contrib-cssmin": "~1.0.2", "grunt-exec": "^1.0.1", - "grunt-karma": "~2.0.0", "grunt-notify": "^0.4.5", "grunt-postcss": "^0.8.0", "grunt-sass": "^2.0.0", @@ -58,14 +57,6 @@ "html-webpack-plugin": "^3.2.0", "husky": "^0.14.3", "jest": "^22.0.4", - "karma": "1.7.0", - "karma-chrome-launcher": "~2.2.0", - "karma-expect": "~1.1.3", - "karma-mocha": "~1.3.0", - "karma-phantomjs-launcher": "1.0.4", - "karma-sinon": "^1.0.5", - "karma-sourcemap-loader": "^0.3.7", - "karma-webpack": "^3.0.0", "lint-staged": "^6.0.0", "load-grunt-tasks": "3.5.2", "mini-css-extract-plugin": "^0.4.0", @@ -112,7 +103,6 @@ "test": "grunt test", "test:coverage": "grunt test --coverage=true", "lint": "tslint -c tslint.json --project tsconfig.json --type-check", - "karma": "grunt karma:dev", "jest": "jest --notify --watch", "api-tests": "jest --notify --watch --config=tests/api/jest.js", "precommit": "lint-staged && grunt precommit" diff --git a/scripts/grunt/default_task.js b/scripts/grunt/default_task.js index efcdcd02963..07519cdd6c8 100644 --- a/scripts/grunt/default_task.js +++ b/scripts/grunt/default_task.js @@ -12,7 +12,6 @@ module.exports = function(grunt) { 'sasslint', 'exec:tslint', "exec:jest", - 'karma:test', 'no-only-tests' ]); diff --git a/scripts/grunt/options/karma.js b/scripts/grunt/options/karma.js deleted file mode 100644 index 9f638d2e36d..00000000000 --- a/scripts/grunt/options/karma.js +++ /dev/null @@ -1,23 +0,0 @@ -module.exports = function (config) { - 'use strict'; - - return { - dev: { - configFile: 'karma.conf.js', - singleRun: false, - }, - - debug: { - configFile: 'karma.conf.js', - singleRun: false, - browsers: ['Chrome'], - mime: { - 'text/x-typescript': ['ts', 'tsx'] - }, - }, - - test: { - configFile: 'karma.conf.js', - } - }; -}; From 837388d13e0a0a84c4829edf4eca285321079f5e Mon Sep 17 00:00:00 2001 From: Sven Klemm Date: Tue, 14 Aug 2018 09:44:58 +0200 Subject: [PATCH 415/786] Use variable in newPostgresMacroEngine --- pkg/tsdb/postgres/macros_test.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/pkg/tsdb/postgres/macros_test.go b/pkg/tsdb/postgres/macros_test.go index 30a57a7095f..f0c8832dd05 100644 --- a/pkg/tsdb/postgres/macros_test.go +++ b/pkg/tsdb/postgres/macros_test.go @@ -12,8 +12,10 @@ import ( func TestMacroEngine(t *testing.T) { Convey("MacroEngine", t, func() { - engine := newPostgresMacroEngine(false) - engineTS := newPostgresMacroEngine(true) + timescaledbEnabled := false + engine := newPostgresMacroEngine(timescaledbEnabled) + timescaledbEnabled = true + engineTS := newPostgresMacroEngine(timescaledbEnabled) query := &tsdb.Query{} Convey("Given a time range between 2018-04-12 00:00 and 2018-04-12 00:05", func() { From d33019ca6740e55d89119bbf6fce32056cdded3f Mon Sep 17 00:00:00 2001 From: Sven Klemm Date: Tue, 14 Aug 2018 10:22:57 +0200 Subject: [PATCH 416/786] document TimescaleDB datasource option --- docs/sources/features/datasources/postgres.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/sources/features/datasources/postgres.md b/docs/sources/features/datasources/postgres.md index 2be2db0837b..e8ed742f64f 100644 --- a/docs/sources/features/datasources/postgres.md +++ b/docs/sources/features/datasources/postgres.md @@ -31,6 +31,7 @@ Name | Description *User* | Database user's login/username *Password* | Database user's password *SSL Mode* | This option determines whether or with what priority a secure SSL TCP/IP connection will be negotiated with the server. +*TimescaleDB* | With this option enabled Grafana will use TimescaleDB features, e.g. use ```time_bucket``` for grouping by time. ### Database User Permissions (Important!) From a96d97e347ad8a8725ca7f8fbb80271812d7e64c Mon Sep 17 00:00:00 2001 From: Sven Klemm Date: Tue, 14 Aug 2018 10:26:08 +0200 Subject: [PATCH 417/786] add version disclaimer for TimescaleDB --- docs/sources/features/datasources/postgres.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/sources/features/datasources/postgres.md b/docs/sources/features/datasources/postgres.md index e8ed742f64f..e2dcf888025 100644 --- a/docs/sources/features/datasources/postgres.md +++ b/docs/sources/features/datasources/postgres.md @@ -31,7 +31,7 @@ Name | Description *User* | Database user's login/username *Password* | Database user's password *SSL Mode* | This option determines whether or with what priority a secure SSL TCP/IP connection will be negotiated with the server. -*TimescaleDB* | With this option enabled Grafana will use TimescaleDB features, e.g. use ```time_bucket``` for grouping by time. +*TimescaleDB* | With this option enabled Grafana will use TimescaleDB features, e.g. use ```time_bucket``` for grouping by time (only available in Grafana 5.3+). ### Database User Permissions (Important!) From b70d594c103de35875600cddabe9130468435cb6 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Tue, 14 Aug 2018 10:35:34 +0200 Subject: [PATCH 418/786] changelog: add notes about closing #12598 [skip ci] --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7cd75402946..0c397e45ea4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -48,6 +48,7 @@ om/grafana/grafana/issues/12668) * **Plugins**: Convert URL-like text to links in plugins readme [#12843](https://github.com/grafana/grafana/pull/12843), thx [pgiraud](https://github.com/pgiraud) * **Docker**: Make it possible to set a specific plugin url [#12861](https://github.com/grafana/grafana/pull/12861), thx [ClementGautier](https://github.com/ClementGautier) * **Graphite**: Fix for quoting of int function parameters (when using variables) [#11927](https://github.com/grafana/grafana/pull/11927) +* **InfluxDB**: Support timeFilter in query templating for InfluxDB [#12598](https://github.com/grafana/grafana/pull/12598), thx [kichristensen](https://github.com/kichristensen) ### Breaking changes From a65589a5fbeb2fde7e5cc2dd6613fb8bf0355ae5 Mon Sep 17 00:00:00 2001 From: Tobias Skarhed Date: Tue, 14 Aug 2018 10:52:41 +0200 Subject: [PATCH 419/786] Rename test files --- .github/CONTRIBUTING.md | 2 +- jest.config.js | 2 +- karma.conf.js | 40 ------------------- ...leList.jest.tsx => AlertRuleList.test.tsx} | 0 ...t.tsx.snap => AlertRuleList.test.tsx.snap} | 0 ...Field.jest.tsx => PromQueryField.test.tsx} | 0 ...imePicker.jest.tsx => TimePicker.test.tsx} | 0 .../{braces.jest.ts => braces.test.ts} | 0 .../{clear.jest.ts => clear.test.ts} | 0 ...{prometheus.jest.ts => prometheus.test.ts} | 0 ...tings.jest.tsx => FolderSettings.test.tsx} | 0 ...verStats.jest.tsx => ServerStats.test.tsx} | 0 ...est.tsx.snap => ServerStats.test.tsx.snap} | 0 ...eButton.jest.tsx => DeleteButton.test.tsx} | 0 ...ListCTA.jest.tsx => EmptyListCTA.test.tsx} | 0 ...st.tsx.snap => EmptyListCTA.test.tsx.snap} | 0 ...ageHeader.jest.tsx => PageHeader.test.tsx} | 0 ...sions.jest.tsx => AddPermissions.test.tsx} | 0 ...rOption.jest.tsx => PickerOption.test.tsx} | 0 ...eamPicker.jest.tsx => TeamPicker.test.tsx} | 0 ...serPicker.jest.tsx => UserPicker.test.tsx} | 0 ...st.tsx.snap => PickerOption.test.tsx.snap} | 0 ...jest.tsx.snap => TeamPicker.test.tsx.snap} | 0 ...jest.tsx.snap => UserPicker.test.tsx.snap} | 0 .../{Popover.jest.tsx => Popover.test.tsx} | 0 .../{Tooltip.jest.tsx => Tooltip.test.tsx} | 0 ...er.jest.tsx.snap => Popover.test.tsx.snap} | 0 ...ip.jest.tsx.snap => Tooltip.test.tsx.snap} | 0 ...Palette.jest.tsx => ColorPalette.test.tsx} | 0 ...gth.jest.tsx => PasswordStrength.test.tsx} | 0 ...st.tsx.snap => ColorPalette.test.tsx.snap} | 0 ...ackend_srv.jest.ts => backend_srv.test.ts} | 0 .../{datemath.jest.ts => datemath.test.ts} | 0 .../{emitter.jest.ts => emitter.test.ts} | 0 ...ile_export.jest.ts => file_export.test.ts} | 0 .../{flatten.jest.ts => flatten.test.ts} | 0 .../core/specs/{kbn.jest.ts => kbn.test.ts} | 0 ...ion_util.jest.ts => location_util.test.ts} | 0 ...ards.jest.ts => manage_dashboards.test.ts} | 0 ..._switcher.jest.ts => org_switcher.test.ts} | 0 .../{rangeutil.jest.ts => rangeutil.test.ts} | 0 .../specs/{search.jest.ts => search.test.ts} | 0 ...results.jest.ts => search_results.test.ts} | 0 ...{search_srv.jest.ts => search_srv.test.ts} | 0 .../specs/{store.jest.ts => store.test.ts} | 0 ...able_model.jest.ts => table_model.test.ts} | 0 .../specs/{ticks.jest.ts => ticks.test.ts} | 0 ...ime_series.jest.ts => time_series.test.ts} | 0 ....jest.ts => value_select_dropdown.test.ts} | 0 ...apper.jest.ts => threshold_mapper.test.ts} | 0 ...ns_srv.jest.ts => annotations_srv.test.ts} | 0 ....jest.ts => annotations_srv_specs.test.ts} | 0 ...lPanel.jest.tsx => AddPanelPanel.test.tsx} | 0 ...oardRow.jest.tsx => DashboardRow.test.tsx} | 0 ...tracker.jest.ts => change_tracker.test.ts} | 0 ....jest.ts => dashboard_import_ctrl.test.ts} | 0 ...on.jest.ts => dashboard_migration.test.ts} | 0 ..._model.jest.ts => dashboard_model.test.ts} | 0 .../{exporter.jest.ts => exporter.test.ts} | 0 ...tory_ctrl.jest.ts => history_ctrl.test.ts} | 0 ...istory_srv.jest.ts => history_srv.test.ts} | 0 .../specs/{repeat.jest.ts => repeat.test.ts} | 0 ...as_modal.jest.ts => save_as_modal.test.ts} | 0 ...{save_modal.jest.ts => save_modal.test.ts} | 0 ...jest.ts => save_provisioned_modal.test.ts} | 0 .../{time_srv.jest.ts => time_srv.test.ts} | 0 ...tate_srv.jest.ts => viewstate_srv.test.ts} | 0 ...trl.jest.ts => metrics_panel_ctrl.test.ts} | 0 .../{link_srv.jest.ts => link_srv.test.ts} | 0 ...trl.jest.ts => playlist_edit_ctrl.test.ts} | 0 ...rce_srv.jest.ts => datasource_srv.test.ts} | 0 ...ariable.jest.ts => adhoc_variable.test.ts} | 0 ...ditor_ctrl.jest.ts => editor_ctrl.test.ts} | 0 ...ariable.jest.ts => query_variable.test.ts} | 0 ...plate_srv.jest.ts => template_srv.test.ts} | 0 .../{variable.jest.ts => variable.test.ts} | 0 ...iable_srv.jest.ts => variable_srv.test.ts} | 0 ...init.jest.ts => variable_srv_init.test.ts} | 0 ...{datasource.jest.ts => datasource.test.ts} | 0 ...{datasource.jest.ts => datasource.test.ts} | 0 ...ponse.jest.ts => elastic_response.test.ts} | 0 ..._pattern.jest.ts => index_pattern.test.ts} | 0 ..._builder.jest.ts => query_builder.test.ts} | 0 .../{query_def.jest.ts => query_def.test.ts} | 0 ...{datasource.jest.ts => datasource.test.ts} | 0 .../specs/{gfunc.jest.ts => gfunc.test.ts} | 0 ...e_query.jest.ts => graphite_query.test.ts} | 0 .../specs/{lexer.jest.ts => lexer.test.ts} | 0 .../specs/{parser.jest.ts => parser.test.ts} | 0 ...{query_ctrl.jest.ts => query_ctrl.test.ts} | 0 ...lux_query.jest.ts => influx_query.test.ts} | 0 ...x_series.jest.ts => influx_series.test.ts} | 0 ..._builder.jest.ts => query_builder.test.ts} | 0 ...{query_ctrl.jest.ts => query_ctrl.test.ts} | 0 ...{query_part.jest.ts => query_part.test.ts} | 0 ...parser.jest.ts => response_parser.test.ts} | 0 ...{datasource.jest.ts => datasource.test.ts} | 0 ...mer.jest.ts => result_transformer.test.ts} | 0 ...{datasource.jest.ts => datasource.test.ts} | 0 ...{datasource.jest.ts => datasource.test.ts} | 0 ...{datasource.jest.ts => datasource.test.ts} | 0 ...{query_ctrl.jest.ts => query_ctrl.test.ts} | 0 ...{datasource.jest.ts => datasource.test.ts} | 0 .../{completer.jest.ts => completer.test.ts} | 0 ...{datasource.jest.ts => datasource.test.ts} | 0 ...uery.jest.ts => metric_find_query.test.ts} | 0 ...mer.jest.ts => result_transformer.test.ts} | 0 ...lign_yaxes.jest.ts => align_yaxes.test.ts} | 0 ...ocessor.jest.ts => data_processor.test.ts} | 0 .../specs/{graph.jest.ts => graph.test.ts} | 0 ...{graph_ctrl.jest.ts => graph_ctrl.test.ts} | 0 ..._tooltip.jest.ts => graph_tooltip.test.ts} | 0 .../{histogram.jest.ts => histogram.test.ts} | 0 ...l.jest.ts => series_override_ctrl.test.ts} | 0 ...ager.jest.ts => threshold_manager.test.ts} | 0 ...tmap_ctrl.jest.ts => heatmap_ctrl.test.ts} | 0 ...jest.ts => heatmap_data_converter.test.ts} | 0 ...{singlestat.jest.ts => singlestat.test.ts} | 0 ...panel.jest.ts => singlestat_panel.test.ts} | 0 .../{renderer.jest.ts => renderer.test.ts} | 0 ...nsformers.jest.ts => transformers.test.ts} | 0 ...stStore.jest.ts => AlertListStore.test.ts} | 0 .../{NavStore.jest.ts => NavStore.test.ts} | 0 ...Store.jest.ts => PermissionsStore.test.ts} | 0 .../{ViewStore.jest.ts => ViewStore.test.ts} | 0 .../{version_jest.ts => version_test.ts} | 0 126 files changed, 2 insertions(+), 42 deletions(-) delete mode 100644 karma.conf.js rename public/app/containers/AlertRuleList/{AlertRuleList.jest.tsx => AlertRuleList.test.tsx} (100%) rename public/app/containers/AlertRuleList/__snapshots__/{AlertRuleList.jest.tsx.snap => AlertRuleList.test.tsx.snap} (100%) rename public/app/containers/Explore/{PromQueryField.jest.tsx => PromQueryField.test.tsx} (100%) rename public/app/containers/Explore/{TimePicker.jest.tsx => TimePicker.test.tsx} (100%) rename public/app/containers/Explore/slate-plugins/{braces.jest.ts => braces.test.ts} (100%) rename public/app/containers/Explore/slate-plugins/{clear.jest.ts => clear.test.ts} (100%) rename public/app/containers/Explore/utils/{prometheus.jest.ts => prometheus.test.ts} (100%) rename public/app/containers/ManageDashboards/{FolderSettings.jest.tsx => FolderSettings.test.tsx} (100%) rename public/app/containers/ServerStats/{ServerStats.jest.tsx => ServerStats.test.tsx} (100%) rename public/app/containers/ServerStats/__snapshots__/{ServerStats.jest.tsx.snap => ServerStats.test.tsx.snap} (100%) rename public/app/core/components/DeleteButton/{DeleteButton.jest.tsx => DeleteButton.test.tsx} (100%) rename public/app/core/components/EmptyListCTA/{EmptyListCTA.jest.tsx => EmptyListCTA.test.tsx} (100%) rename public/app/core/components/EmptyListCTA/__snapshots__/{EmptyListCTA.jest.tsx.snap => EmptyListCTA.test.tsx.snap} (100%) rename public/app/core/components/PageHeader/{PageHeader.jest.tsx => PageHeader.test.tsx} (100%) rename public/app/core/components/Permissions/{AddPermissions.jest.tsx => AddPermissions.test.tsx} (100%) rename public/app/core/components/Picker/{PickerOption.jest.tsx => PickerOption.test.tsx} (100%) rename public/app/core/components/Picker/{TeamPicker.jest.tsx => TeamPicker.test.tsx} (100%) rename public/app/core/components/Picker/{UserPicker.jest.tsx => UserPicker.test.tsx} (100%) rename public/app/core/components/Picker/__snapshots__/{PickerOption.jest.tsx.snap => PickerOption.test.tsx.snap} (100%) rename public/app/core/components/Picker/__snapshots__/{TeamPicker.jest.tsx.snap => TeamPicker.test.tsx.snap} (100%) rename public/app/core/components/Picker/__snapshots__/{UserPicker.jest.tsx.snap => UserPicker.test.tsx.snap} (100%) rename public/app/core/components/Tooltip/{Popover.jest.tsx => Popover.test.tsx} (100%) rename public/app/core/components/Tooltip/{Tooltip.jest.tsx => Tooltip.test.tsx} (100%) rename public/app/core/components/Tooltip/__snapshots__/{Popover.jest.tsx.snap => Popover.test.tsx.snap} (100%) rename public/app/core/components/Tooltip/__snapshots__/{Tooltip.jest.tsx.snap => Tooltip.test.tsx.snap} (100%) rename public/app/core/specs/{ColorPalette.jest.tsx => ColorPalette.test.tsx} (100%) rename public/app/core/specs/{PasswordStrength.jest.tsx => PasswordStrength.test.tsx} (100%) rename public/app/core/specs/__snapshots__/{ColorPalette.jest.tsx.snap => ColorPalette.test.tsx.snap} (100%) rename public/app/core/specs/{backend_srv.jest.ts => backend_srv.test.ts} (100%) rename public/app/core/specs/{datemath.jest.ts => datemath.test.ts} (100%) rename public/app/core/specs/{emitter.jest.ts => emitter.test.ts} (100%) rename public/app/core/specs/{file_export.jest.ts => file_export.test.ts} (100%) rename public/app/core/specs/{flatten.jest.ts => flatten.test.ts} (100%) rename public/app/core/specs/{kbn.jest.ts => kbn.test.ts} (100%) rename public/app/core/specs/{location_util.jest.ts => location_util.test.ts} (100%) rename public/app/core/specs/{manage_dashboards.jest.ts => manage_dashboards.test.ts} (100%) rename public/app/core/specs/{org_switcher.jest.ts => org_switcher.test.ts} (100%) rename public/app/core/specs/{rangeutil.jest.ts => rangeutil.test.ts} (100%) rename public/app/core/specs/{search.jest.ts => search.test.ts} (100%) rename public/app/core/specs/{search_results.jest.ts => search_results.test.ts} (100%) rename public/app/core/specs/{search_srv.jest.ts => search_srv.test.ts} (100%) rename public/app/core/specs/{store.jest.ts => store.test.ts} (100%) rename public/app/core/specs/{table_model.jest.ts => table_model.test.ts} (100%) rename public/app/core/specs/{ticks.jest.ts => ticks.test.ts} (100%) rename public/app/core/specs/{time_series.jest.ts => time_series.test.ts} (100%) rename public/app/core/specs/{value_select_dropdown.jest.ts => value_select_dropdown.test.ts} (100%) rename public/app/features/alerting/specs/{threshold_mapper.jest.ts => threshold_mapper.test.ts} (100%) rename public/app/features/annotations/specs/{annotations_srv.jest.ts => annotations_srv.test.ts} (100%) rename public/app/features/annotations/specs/{annotations_srv_specs.jest.ts => annotations_srv_specs.test.ts} (100%) rename public/app/features/dashboard/specs/{AddPanelPanel.jest.tsx => AddPanelPanel.test.tsx} (100%) rename public/app/features/dashboard/specs/{DashboardRow.jest.tsx => DashboardRow.test.tsx} (100%) rename public/app/features/dashboard/specs/{change_tracker.jest.ts => change_tracker.test.ts} (100%) rename public/app/features/dashboard/specs/{dashboard_import_ctrl.jest.ts => dashboard_import_ctrl.test.ts} (100%) rename public/app/features/dashboard/specs/{dashboard_migration.jest.ts => dashboard_migration.test.ts} (100%) rename public/app/features/dashboard/specs/{dashboard_model.jest.ts => dashboard_model.test.ts} (100%) rename public/app/features/dashboard/specs/{exporter.jest.ts => exporter.test.ts} (100%) rename public/app/features/dashboard/specs/{history_ctrl.jest.ts => history_ctrl.test.ts} (100%) rename public/app/features/dashboard/specs/{history_srv.jest.ts => history_srv.test.ts} (100%) rename public/app/features/dashboard/specs/{repeat.jest.ts => repeat.test.ts} (100%) rename public/app/features/dashboard/specs/{save_as_modal.jest.ts => save_as_modal.test.ts} (100%) rename public/app/features/dashboard/specs/{save_modal.jest.ts => save_modal.test.ts} (100%) rename public/app/features/dashboard/specs/{save_provisioned_modal.jest.ts => save_provisioned_modal.test.ts} (100%) rename public/app/features/dashboard/specs/{time_srv.jest.ts => time_srv.test.ts} (100%) rename public/app/features/dashboard/specs/{viewstate_srv.jest.ts => viewstate_srv.test.ts} (100%) rename public/app/features/panel/specs/{metrics_panel_ctrl.jest.ts => metrics_panel_ctrl.test.ts} (100%) rename public/app/features/panellinks/specs/{link_srv.jest.ts => link_srv.test.ts} (100%) rename public/app/features/playlist/specs/{playlist_edit_ctrl.jest.ts => playlist_edit_ctrl.test.ts} (100%) rename public/app/features/plugins/specs/{datasource_srv.jest.ts => datasource_srv.test.ts} (100%) rename public/app/features/templating/specs/{adhoc_variable.jest.ts => adhoc_variable.test.ts} (100%) rename public/app/features/templating/specs/{editor_ctrl.jest.ts => editor_ctrl.test.ts} (100%) rename public/app/features/templating/specs/{query_variable.jest.ts => query_variable.test.ts} (100%) rename public/app/features/templating/specs/{template_srv.jest.ts => template_srv.test.ts} (100%) rename public/app/features/templating/specs/{variable.jest.ts => variable.test.ts} (100%) rename public/app/features/templating/specs/{variable_srv.jest.ts => variable_srv.test.ts} (100%) rename public/app/features/templating/specs/{variable_srv_init.jest.ts => variable_srv_init.test.ts} (100%) rename public/app/plugins/datasource/cloudwatch/specs/{datasource.jest.ts => datasource.test.ts} (100%) rename public/app/plugins/datasource/elasticsearch/specs/{datasource.jest.ts => datasource.test.ts} (100%) rename public/app/plugins/datasource/elasticsearch/specs/{elastic_response.jest.ts => elastic_response.test.ts} (100%) rename public/app/plugins/datasource/elasticsearch/specs/{index_pattern.jest.ts => index_pattern.test.ts} (100%) rename public/app/plugins/datasource/elasticsearch/specs/{query_builder.jest.ts => query_builder.test.ts} (100%) rename public/app/plugins/datasource/elasticsearch/specs/{query_def.jest.ts => query_def.test.ts} (100%) rename public/app/plugins/datasource/graphite/specs/{datasource.jest.ts => datasource.test.ts} (100%) rename public/app/plugins/datasource/graphite/specs/{gfunc.jest.ts => gfunc.test.ts} (100%) rename public/app/plugins/datasource/graphite/specs/{graphite_query.jest.ts => graphite_query.test.ts} (100%) rename public/app/plugins/datasource/graphite/specs/{lexer.jest.ts => lexer.test.ts} (100%) rename public/app/plugins/datasource/graphite/specs/{parser.jest.ts => parser.test.ts} (100%) rename public/app/plugins/datasource/graphite/specs/{query_ctrl.jest.ts => query_ctrl.test.ts} (100%) rename public/app/plugins/datasource/influxdb/specs/{influx_query.jest.ts => influx_query.test.ts} (100%) rename public/app/plugins/datasource/influxdb/specs/{influx_series.jest.ts => influx_series.test.ts} (100%) rename public/app/plugins/datasource/influxdb/specs/{query_builder.jest.ts => query_builder.test.ts} (100%) rename public/app/plugins/datasource/influxdb/specs/{query_ctrl.jest.ts => query_ctrl.test.ts} (100%) rename public/app/plugins/datasource/influxdb/specs/{query_part.jest.ts => query_part.test.ts} (100%) rename public/app/plugins/datasource/influxdb/specs/{response_parser.jest.ts => response_parser.test.ts} (100%) rename public/app/plugins/datasource/logging/{datasource.jest.ts => datasource.test.ts} (100%) rename public/app/plugins/datasource/logging/{result_transformer.jest.ts => result_transformer.test.ts} (100%) rename public/app/plugins/datasource/mssql/specs/{datasource.jest.ts => datasource.test.ts} (100%) rename public/app/plugins/datasource/mysql/specs/{datasource.jest.ts => datasource.test.ts} (100%) rename public/app/plugins/datasource/opentsdb/specs/{datasource.jest.ts => datasource.test.ts} (100%) rename public/app/plugins/datasource/opentsdb/specs/{query_ctrl.jest.ts => query_ctrl.test.ts} (100%) rename public/app/plugins/datasource/postgres/specs/{datasource.jest.ts => datasource.test.ts} (100%) rename public/app/plugins/datasource/prometheus/specs/{completer.jest.ts => completer.test.ts} (100%) rename public/app/plugins/datasource/prometheus/specs/{datasource.jest.ts => datasource.test.ts} (100%) rename public/app/plugins/datasource/prometheus/specs/{metric_find_query.jest.ts => metric_find_query.test.ts} (100%) rename public/app/plugins/datasource/prometheus/specs/{result_transformer.jest.ts => result_transformer.test.ts} (100%) rename public/app/plugins/panel/graph/specs/{align_yaxes.jest.ts => align_yaxes.test.ts} (100%) rename public/app/plugins/panel/graph/specs/{data_processor.jest.ts => data_processor.test.ts} (100%) rename public/app/plugins/panel/graph/specs/{graph.jest.ts => graph.test.ts} (100%) rename public/app/plugins/panel/graph/specs/{graph_ctrl.jest.ts => graph_ctrl.test.ts} (100%) rename public/app/plugins/panel/graph/specs/{graph_tooltip.jest.ts => graph_tooltip.test.ts} (100%) rename public/app/plugins/panel/graph/specs/{histogram.jest.ts => histogram.test.ts} (100%) rename public/app/plugins/panel/graph/specs/{series_override_ctrl.jest.ts => series_override_ctrl.test.ts} (100%) rename public/app/plugins/panel/graph/specs/{threshold_manager.jest.ts => threshold_manager.test.ts} (100%) rename public/app/plugins/panel/heatmap/specs/{heatmap_ctrl.jest.ts => heatmap_ctrl.test.ts} (100%) rename public/app/plugins/panel/heatmap/specs/{heatmap_data_converter.jest.ts => heatmap_data_converter.test.ts} (100%) rename public/app/plugins/panel/singlestat/specs/{singlestat.jest.ts => singlestat.test.ts} (100%) rename public/app/plugins/panel/singlestat/specs/{singlestat_panel.jest.ts => singlestat_panel.test.ts} (100%) rename public/app/plugins/panel/table/specs/{renderer.jest.ts => renderer.test.ts} (100%) rename public/app/plugins/panel/table/specs/{transformers.jest.ts => transformers.test.ts} (100%) rename public/app/stores/AlertListStore/{AlertListStore.jest.ts => AlertListStore.test.ts} (100%) rename public/app/stores/NavStore/{NavStore.jest.ts => NavStore.test.ts} (100%) rename public/app/stores/PermissionsStore/{PermissionsStore.jest.ts => PermissionsStore.test.ts} (100%) rename public/app/stores/ViewStore/{ViewStore.jest.ts => ViewStore.test.ts} (100%) rename public/test/core/utils/{version_jest.ts => version_test.ts} (100%) diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index f0f4e19bfc3..14c6c07ab16 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -7,7 +7,7 @@ grunt && grunt watch ### Rerun tests on source change ``` -npm jest +npm run jest ``` or ``` diff --git a/jest.config.js b/jest.config.js index 606465c9840..a5cd3416f75 100644 --- a/jest.config.js +++ b/jest.config.js @@ -13,7 +13,7 @@ module.exports = { "roots": [ "/public" ], - "testRegex": "(\\.|/)(jest)\\.(jsx?|tsx?)$", + "testRegex": "(\\.|/)(test)\\.(jsx?|tsx?)$", "moduleFileExtensions": [ "ts", "tsx", diff --git a/karma.conf.js b/karma.conf.js deleted file mode 100644 index 352e8e4e027..00000000000 --- a/karma.conf.js +++ /dev/null @@ -1,40 +0,0 @@ -var webpack = require('webpack'); -var path = require('path'); -var webpackTestConfig = require('./scripts/webpack/webpack.test.js'); - -module.exports = function(config) { - - 'use strict'; - - config.set({ - frameworks: ['mocha', 'expect', 'sinon'], - - // list of files / patterns to load in the browser - files: [ - { pattern: 'public/test/index.ts', watched: false } - ], - - preprocessors: { - 'public/test/index.ts': ['webpack', 'sourcemap'], - }, - - webpack: webpackTestConfig, - webpackMiddleware: { - stats: 'minimal', - }, - - // list of files to exclude - exclude: [], - reporters: ['dots'], - port: 9876, - colors: true, - logLevel: config.LOG_INFO, - autoWatch: true, - browsers: ['PhantomJS'], - captureTimeout: 20000, - singleRun: true, - // autoWatchBatchDelay: 1000, - // browserNoActivityTimeout: 60000, - }); - -}; diff --git a/public/app/containers/AlertRuleList/AlertRuleList.jest.tsx b/public/app/containers/AlertRuleList/AlertRuleList.test.tsx similarity index 100% rename from public/app/containers/AlertRuleList/AlertRuleList.jest.tsx rename to public/app/containers/AlertRuleList/AlertRuleList.test.tsx diff --git a/public/app/containers/AlertRuleList/__snapshots__/AlertRuleList.jest.tsx.snap b/public/app/containers/AlertRuleList/__snapshots__/AlertRuleList.test.tsx.snap similarity index 100% rename from public/app/containers/AlertRuleList/__snapshots__/AlertRuleList.jest.tsx.snap rename to public/app/containers/AlertRuleList/__snapshots__/AlertRuleList.test.tsx.snap diff --git a/public/app/containers/Explore/PromQueryField.jest.tsx b/public/app/containers/Explore/PromQueryField.test.tsx similarity index 100% rename from public/app/containers/Explore/PromQueryField.jest.tsx rename to public/app/containers/Explore/PromQueryField.test.tsx diff --git a/public/app/containers/Explore/TimePicker.jest.tsx b/public/app/containers/Explore/TimePicker.test.tsx similarity index 100% rename from public/app/containers/Explore/TimePicker.jest.tsx rename to public/app/containers/Explore/TimePicker.test.tsx diff --git a/public/app/containers/Explore/slate-plugins/braces.jest.ts b/public/app/containers/Explore/slate-plugins/braces.test.ts similarity index 100% rename from public/app/containers/Explore/slate-plugins/braces.jest.ts rename to public/app/containers/Explore/slate-plugins/braces.test.ts diff --git a/public/app/containers/Explore/slate-plugins/clear.jest.ts b/public/app/containers/Explore/slate-plugins/clear.test.ts similarity index 100% rename from public/app/containers/Explore/slate-plugins/clear.jest.ts rename to public/app/containers/Explore/slate-plugins/clear.test.ts diff --git a/public/app/containers/Explore/utils/prometheus.jest.ts b/public/app/containers/Explore/utils/prometheus.test.ts similarity index 100% rename from public/app/containers/Explore/utils/prometheus.jest.ts rename to public/app/containers/Explore/utils/prometheus.test.ts diff --git a/public/app/containers/ManageDashboards/FolderSettings.jest.tsx b/public/app/containers/ManageDashboards/FolderSettings.test.tsx similarity index 100% rename from public/app/containers/ManageDashboards/FolderSettings.jest.tsx rename to public/app/containers/ManageDashboards/FolderSettings.test.tsx diff --git a/public/app/containers/ServerStats/ServerStats.jest.tsx b/public/app/containers/ServerStats/ServerStats.test.tsx similarity index 100% rename from public/app/containers/ServerStats/ServerStats.jest.tsx rename to public/app/containers/ServerStats/ServerStats.test.tsx diff --git a/public/app/containers/ServerStats/__snapshots__/ServerStats.jest.tsx.snap b/public/app/containers/ServerStats/__snapshots__/ServerStats.test.tsx.snap similarity index 100% rename from public/app/containers/ServerStats/__snapshots__/ServerStats.jest.tsx.snap rename to public/app/containers/ServerStats/__snapshots__/ServerStats.test.tsx.snap diff --git a/public/app/core/components/DeleteButton/DeleteButton.jest.tsx b/public/app/core/components/DeleteButton/DeleteButton.test.tsx similarity index 100% rename from public/app/core/components/DeleteButton/DeleteButton.jest.tsx rename to public/app/core/components/DeleteButton/DeleteButton.test.tsx diff --git a/public/app/core/components/EmptyListCTA/EmptyListCTA.jest.tsx b/public/app/core/components/EmptyListCTA/EmptyListCTA.test.tsx similarity index 100% rename from public/app/core/components/EmptyListCTA/EmptyListCTA.jest.tsx rename to public/app/core/components/EmptyListCTA/EmptyListCTA.test.tsx diff --git a/public/app/core/components/EmptyListCTA/__snapshots__/EmptyListCTA.jest.tsx.snap b/public/app/core/components/EmptyListCTA/__snapshots__/EmptyListCTA.test.tsx.snap similarity index 100% rename from public/app/core/components/EmptyListCTA/__snapshots__/EmptyListCTA.jest.tsx.snap rename to public/app/core/components/EmptyListCTA/__snapshots__/EmptyListCTA.test.tsx.snap diff --git a/public/app/core/components/PageHeader/PageHeader.jest.tsx b/public/app/core/components/PageHeader/PageHeader.test.tsx similarity index 100% rename from public/app/core/components/PageHeader/PageHeader.jest.tsx rename to public/app/core/components/PageHeader/PageHeader.test.tsx diff --git a/public/app/core/components/Permissions/AddPermissions.jest.tsx b/public/app/core/components/Permissions/AddPermissions.test.tsx similarity index 100% rename from public/app/core/components/Permissions/AddPermissions.jest.tsx rename to public/app/core/components/Permissions/AddPermissions.test.tsx diff --git a/public/app/core/components/Picker/PickerOption.jest.tsx b/public/app/core/components/Picker/PickerOption.test.tsx similarity index 100% rename from public/app/core/components/Picker/PickerOption.jest.tsx rename to public/app/core/components/Picker/PickerOption.test.tsx diff --git a/public/app/core/components/Picker/TeamPicker.jest.tsx b/public/app/core/components/Picker/TeamPicker.test.tsx similarity index 100% rename from public/app/core/components/Picker/TeamPicker.jest.tsx rename to public/app/core/components/Picker/TeamPicker.test.tsx diff --git a/public/app/core/components/Picker/UserPicker.jest.tsx b/public/app/core/components/Picker/UserPicker.test.tsx similarity index 100% rename from public/app/core/components/Picker/UserPicker.jest.tsx rename to public/app/core/components/Picker/UserPicker.test.tsx diff --git a/public/app/core/components/Picker/__snapshots__/PickerOption.jest.tsx.snap b/public/app/core/components/Picker/__snapshots__/PickerOption.test.tsx.snap similarity index 100% rename from public/app/core/components/Picker/__snapshots__/PickerOption.jest.tsx.snap rename to public/app/core/components/Picker/__snapshots__/PickerOption.test.tsx.snap diff --git a/public/app/core/components/Picker/__snapshots__/TeamPicker.jest.tsx.snap b/public/app/core/components/Picker/__snapshots__/TeamPicker.test.tsx.snap similarity index 100% rename from public/app/core/components/Picker/__snapshots__/TeamPicker.jest.tsx.snap rename to public/app/core/components/Picker/__snapshots__/TeamPicker.test.tsx.snap diff --git a/public/app/core/components/Picker/__snapshots__/UserPicker.jest.tsx.snap b/public/app/core/components/Picker/__snapshots__/UserPicker.test.tsx.snap similarity index 100% rename from public/app/core/components/Picker/__snapshots__/UserPicker.jest.tsx.snap rename to public/app/core/components/Picker/__snapshots__/UserPicker.test.tsx.snap diff --git a/public/app/core/components/Tooltip/Popover.jest.tsx b/public/app/core/components/Tooltip/Popover.test.tsx similarity index 100% rename from public/app/core/components/Tooltip/Popover.jest.tsx rename to public/app/core/components/Tooltip/Popover.test.tsx diff --git a/public/app/core/components/Tooltip/Tooltip.jest.tsx b/public/app/core/components/Tooltip/Tooltip.test.tsx similarity index 100% rename from public/app/core/components/Tooltip/Tooltip.jest.tsx rename to public/app/core/components/Tooltip/Tooltip.test.tsx diff --git a/public/app/core/components/Tooltip/__snapshots__/Popover.jest.tsx.snap b/public/app/core/components/Tooltip/__snapshots__/Popover.test.tsx.snap similarity index 100% rename from public/app/core/components/Tooltip/__snapshots__/Popover.jest.tsx.snap rename to public/app/core/components/Tooltip/__snapshots__/Popover.test.tsx.snap diff --git a/public/app/core/components/Tooltip/__snapshots__/Tooltip.jest.tsx.snap b/public/app/core/components/Tooltip/__snapshots__/Tooltip.test.tsx.snap similarity index 100% rename from public/app/core/components/Tooltip/__snapshots__/Tooltip.jest.tsx.snap rename to public/app/core/components/Tooltip/__snapshots__/Tooltip.test.tsx.snap diff --git a/public/app/core/specs/ColorPalette.jest.tsx b/public/app/core/specs/ColorPalette.test.tsx similarity index 100% rename from public/app/core/specs/ColorPalette.jest.tsx rename to public/app/core/specs/ColorPalette.test.tsx diff --git a/public/app/core/specs/PasswordStrength.jest.tsx b/public/app/core/specs/PasswordStrength.test.tsx similarity index 100% rename from public/app/core/specs/PasswordStrength.jest.tsx rename to public/app/core/specs/PasswordStrength.test.tsx diff --git a/public/app/core/specs/__snapshots__/ColorPalette.jest.tsx.snap b/public/app/core/specs/__snapshots__/ColorPalette.test.tsx.snap similarity index 100% rename from public/app/core/specs/__snapshots__/ColorPalette.jest.tsx.snap rename to public/app/core/specs/__snapshots__/ColorPalette.test.tsx.snap diff --git a/public/app/core/specs/backend_srv.jest.ts b/public/app/core/specs/backend_srv.test.ts similarity index 100% rename from public/app/core/specs/backend_srv.jest.ts rename to public/app/core/specs/backend_srv.test.ts diff --git a/public/app/core/specs/datemath.jest.ts b/public/app/core/specs/datemath.test.ts similarity index 100% rename from public/app/core/specs/datemath.jest.ts rename to public/app/core/specs/datemath.test.ts diff --git a/public/app/core/specs/emitter.jest.ts b/public/app/core/specs/emitter.test.ts similarity index 100% rename from public/app/core/specs/emitter.jest.ts rename to public/app/core/specs/emitter.test.ts diff --git a/public/app/core/specs/file_export.jest.ts b/public/app/core/specs/file_export.test.ts similarity index 100% rename from public/app/core/specs/file_export.jest.ts rename to public/app/core/specs/file_export.test.ts diff --git a/public/app/core/specs/flatten.jest.ts b/public/app/core/specs/flatten.test.ts similarity index 100% rename from public/app/core/specs/flatten.jest.ts rename to public/app/core/specs/flatten.test.ts diff --git a/public/app/core/specs/kbn.jest.ts b/public/app/core/specs/kbn.test.ts similarity index 100% rename from public/app/core/specs/kbn.jest.ts rename to public/app/core/specs/kbn.test.ts diff --git a/public/app/core/specs/location_util.jest.ts b/public/app/core/specs/location_util.test.ts similarity index 100% rename from public/app/core/specs/location_util.jest.ts rename to public/app/core/specs/location_util.test.ts diff --git a/public/app/core/specs/manage_dashboards.jest.ts b/public/app/core/specs/manage_dashboards.test.ts similarity index 100% rename from public/app/core/specs/manage_dashboards.jest.ts rename to public/app/core/specs/manage_dashboards.test.ts diff --git a/public/app/core/specs/org_switcher.jest.ts b/public/app/core/specs/org_switcher.test.ts similarity index 100% rename from public/app/core/specs/org_switcher.jest.ts rename to public/app/core/specs/org_switcher.test.ts diff --git a/public/app/core/specs/rangeutil.jest.ts b/public/app/core/specs/rangeutil.test.ts similarity index 100% rename from public/app/core/specs/rangeutil.jest.ts rename to public/app/core/specs/rangeutil.test.ts diff --git a/public/app/core/specs/search.jest.ts b/public/app/core/specs/search.test.ts similarity index 100% rename from public/app/core/specs/search.jest.ts rename to public/app/core/specs/search.test.ts diff --git a/public/app/core/specs/search_results.jest.ts b/public/app/core/specs/search_results.test.ts similarity index 100% rename from public/app/core/specs/search_results.jest.ts rename to public/app/core/specs/search_results.test.ts diff --git a/public/app/core/specs/search_srv.jest.ts b/public/app/core/specs/search_srv.test.ts similarity index 100% rename from public/app/core/specs/search_srv.jest.ts rename to public/app/core/specs/search_srv.test.ts diff --git a/public/app/core/specs/store.jest.ts b/public/app/core/specs/store.test.ts similarity index 100% rename from public/app/core/specs/store.jest.ts rename to public/app/core/specs/store.test.ts diff --git a/public/app/core/specs/table_model.jest.ts b/public/app/core/specs/table_model.test.ts similarity index 100% rename from public/app/core/specs/table_model.jest.ts rename to public/app/core/specs/table_model.test.ts diff --git a/public/app/core/specs/ticks.jest.ts b/public/app/core/specs/ticks.test.ts similarity index 100% rename from public/app/core/specs/ticks.jest.ts rename to public/app/core/specs/ticks.test.ts diff --git a/public/app/core/specs/time_series.jest.ts b/public/app/core/specs/time_series.test.ts similarity index 100% rename from public/app/core/specs/time_series.jest.ts rename to public/app/core/specs/time_series.test.ts diff --git a/public/app/core/specs/value_select_dropdown.jest.ts b/public/app/core/specs/value_select_dropdown.test.ts similarity index 100% rename from public/app/core/specs/value_select_dropdown.jest.ts rename to public/app/core/specs/value_select_dropdown.test.ts diff --git a/public/app/features/alerting/specs/threshold_mapper.jest.ts b/public/app/features/alerting/specs/threshold_mapper.test.ts similarity index 100% rename from public/app/features/alerting/specs/threshold_mapper.jest.ts rename to public/app/features/alerting/specs/threshold_mapper.test.ts diff --git a/public/app/features/annotations/specs/annotations_srv.jest.ts b/public/app/features/annotations/specs/annotations_srv.test.ts similarity index 100% rename from public/app/features/annotations/specs/annotations_srv.jest.ts rename to public/app/features/annotations/specs/annotations_srv.test.ts diff --git a/public/app/features/annotations/specs/annotations_srv_specs.jest.ts b/public/app/features/annotations/specs/annotations_srv_specs.test.ts similarity index 100% rename from public/app/features/annotations/specs/annotations_srv_specs.jest.ts rename to public/app/features/annotations/specs/annotations_srv_specs.test.ts diff --git a/public/app/features/dashboard/specs/AddPanelPanel.jest.tsx b/public/app/features/dashboard/specs/AddPanelPanel.test.tsx similarity index 100% rename from public/app/features/dashboard/specs/AddPanelPanel.jest.tsx rename to public/app/features/dashboard/specs/AddPanelPanel.test.tsx diff --git a/public/app/features/dashboard/specs/DashboardRow.jest.tsx b/public/app/features/dashboard/specs/DashboardRow.test.tsx similarity index 100% rename from public/app/features/dashboard/specs/DashboardRow.jest.tsx rename to public/app/features/dashboard/specs/DashboardRow.test.tsx diff --git a/public/app/features/dashboard/specs/change_tracker.jest.ts b/public/app/features/dashboard/specs/change_tracker.test.ts similarity index 100% rename from public/app/features/dashboard/specs/change_tracker.jest.ts rename to public/app/features/dashboard/specs/change_tracker.test.ts diff --git a/public/app/features/dashboard/specs/dashboard_import_ctrl.jest.ts b/public/app/features/dashboard/specs/dashboard_import_ctrl.test.ts similarity index 100% rename from public/app/features/dashboard/specs/dashboard_import_ctrl.jest.ts rename to public/app/features/dashboard/specs/dashboard_import_ctrl.test.ts diff --git a/public/app/features/dashboard/specs/dashboard_migration.jest.ts b/public/app/features/dashboard/specs/dashboard_migration.test.ts similarity index 100% rename from public/app/features/dashboard/specs/dashboard_migration.jest.ts rename to public/app/features/dashboard/specs/dashboard_migration.test.ts diff --git a/public/app/features/dashboard/specs/dashboard_model.jest.ts b/public/app/features/dashboard/specs/dashboard_model.test.ts similarity index 100% rename from public/app/features/dashboard/specs/dashboard_model.jest.ts rename to public/app/features/dashboard/specs/dashboard_model.test.ts diff --git a/public/app/features/dashboard/specs/exporter.jest.ts b/public/app/features/dashboard/specs/exporter.test.ts similarity index 100% rename from public/app/features/dashboard/specs/exporter.jest.ts rename to public/app/features/dashboard/specs/exporter.test.ts diff --git a/public/app/features/dashboard/specs/history_ctrl.jest.ts b/public/app/features/dashboard/specs/history_ctrl.test.ts similarity index 100% rename from public/app/features/dashboard/specs/history_ctrl.jest.ts rename to public/app/features/dashboard/specs/history_ctrl.test.ts diff --git a/public/app/features/dashboard/specs/history_srv.jest.ts b/public/app/features/dashboard/specs/history_srv.test.ts similarity index 100% rename from public/app/features/dashboard/specs/history_srv.jest.ts rename to public/app/features/dashboard/specs/history_srv.test.ts diff --git a/public/app/features/dashboard/specs/repeat.jest.ts b/public/app/features/dashboard/specs/repeat.test.ts similarity index 100% rename from public/app/features/dashboard/specs/repeat.jest.ts rename to public/app/features/dashboard/specs/repeat.test.ts diff --git a/public/app/features/dashboard/specs/save_as_modal.jest.ts b/public/app/features/dashboard/specs/save_as_modal.test.ts similarity index 100% rename from public/app/features/dashboard/specs/save_as_modal.jest.ts rename to public/app/features/dashboard/specs/save_as_modal.test.ts diff --git a/public/app/features/dashboard/specs/save_modal.jest.ts b/public/app/features/dashboard/specs/save_modal.test.ts similarity index 100% rename from public/app/features/dashboard/specs/save_modal.jest.ts rename to public/app/features/dashboard/specs/save_modal.test.ts diff --git a/public/app/features/dashboard/specs/save_provisioned_modal.jest.ts b/public/app/features/dashboard/specs/save_provisioned_modal.test.ts similarity index 100% rename from public/app/features/dashboard/specs/save_provisioned_modal.jest.ts rename to public/app/features/dashboard/specs/save_provisioned_modal.test.ts diff --git a/public/app/features/dashboard/specs/time_srv.jest.ts b/public/app/features/dashboard/specs/time_srv.test.ts similarity index 100% rename from public/app/features/dashboard/specs/time_srv.jest.ts rename to public/app/features/dashboard/specs/time_srv.test.ts diff --git a/public/app/features/dashboard/specs/viewstate_srv.jest.ts b/public/app/features/dashboard/specs/viewstate_srv.test.ts similarity index 100% rename from public/app/features/dashboard/specs/viewstate_srv.jest.ts rename to public/app/features/dashboard/specs/viewstate_srv.test.ts diff --git a/public/app/features/panel/specs/metrics_panel_ctrl.jest.ts b/public/app/features/panel/specs/metrics_panel_ctrl.test.ts similarity index 100% rename from public/app/features/panel/specs/metrics_panel_ctrl.jest.ts rename to public/app/features/panel/specs/metrics_panel_ctrl.test.ts diff --git a/public/app/features/panellinks/specs/link_srv.jest.ts b/public/app/features/panellinks/specs/link_srv.test.ts similarity index 100% rename from public/app/features/panellinks/specs/link_srv.jest.ts rename to public/app/features/panellinks/specs/link_srv.test.ts diff --git a/public/app/features/playlist/specs/playlist_edit_ctrl.jest.ts b/public/app/features/playlist/specs/playlist_edit_ctrl.test.ts similarity index 100% rename from public/app/features/playlist/specs/playlist_edit_ctrl.jest.ts rename to public/app/features/playlist/specs/playlist_edit_ctrl.test.ts diff --git a/public/app/features/plugins/specs/datasource_srv.jest.ts b/public/app/features/plugins/specs/datasource_srv.test.ts similarity index 100% rename from public/app/features/plugins/specs/datasource_srv.jest.ts rename to public/app/features/plugins/specs/datasource_srv.test.ts diff --git a/public/app/features/templating/specs/adhoc_variable.jest.ts b/public/app/features/templating/specs/adhoc_variable.test.ts similarity index 100% rename from public/app/features/templating/specs/adhoc_variable.jest.ts rename to public/app/features/templating/specs/adhoc_variable.test.ts diff --git a/public/app/features/templating/specs/editor_ctrl.jest.ts b/public/app/features/templating/specs/editor_ctrl.test.ts similarity index 100% rename from public/app/features/templating/specs/editor_ctrl.jest.ts rename to public/app/features/templating/specs/editor_ctrl.test.ts diff --git a/public/app/features/templating/specs/query_variable.jest.ts b/public/app/features/templating/specs/query_variable.test.ts similarity index 100% rename from public/app/features/templating/specs/query_variable.jest.ts rename to public/app/features/templating/specs/query_variable.test.ts diff --git a/public/app/features/templating/specs/template_srv.jest.ts b/public/app/features/templating/specs/template_srv.test.ts similarity index 100% rename from public/app/features/templating/specs/template_srv.jest.ts rename to public/app/features/templating/specs/template_srv.test.ts diff --git a/public/app/features/templating/specs/variable.jest.ts b/public/app/features/templating/specs/variable.test.ts similarity index 100% rename from public/app/features/templating/specs/variable.jest.ts rename to public/app/features/templating/specs/variable.test.ts diff --git a/public/app/features/templating/specs/variable_srv.jest.ts b/public/app/features/templating/specs/variable_srv.test.ts similarity index 100% rename from public/app/features/templating/specs/variable_srv.jest.ts rename to public/app/features/templating/specs/variable_srv.test.ts diff --git a/public/app/features/templating/specs/variable_srv_init.jest.ts b/public/app/features/templating/specs/variable_srv_init.test.ts similarity index 100% rename from public/app/features/templating/specs/variable_srv_init.jest.ts rename to public/app/features/templating/specs/variable_srv_init.test.ts diff --git a/public/app/plugins/datasource/cloudwatch/specs/datasource.jest.ts b/public/app/plugins/datasource/cloudwatch/specs/datasource.test.ts similarity index 100% rename from public/app/plugins/datasource/cloudwatch/specs/datasource.jest.ts rename to public/app/plugins/datasource/cloudwatch/specs/datasource.test.ts diff --git a/public/app/plugins/datasource/elasticsearch/specs/datasource.jest.ts b/public/app/plugins/datasource/elasticsearch/specs/datasource.test.ts similarity index 100% rename from public/app/plugins/datasource/elasticsearch/specs/datasource.jest.ts rename to public/app/plugins/datasource/elasticsearch/specs/datasource.test.ts diff --git a/public/app/plugins/datasource/elasticsearch/specs/elastic_response.jest.ts b/public/app/plugins/datasource/elasticsearch/specs/elastic_response.test.ts similarity index 100% rename from public/app/plugins/datasource/elasticsearch/specs/elastic_response.jest.ts rename to public/app/plugins/datasource/elasticsearch/specs/elastic_response.test.ts diff --git a/public/app/plugins/datasource/elasticsearch/specs/index_pattern.jest.ts b/public/app/plugins/datasource/elasticsearch/specs/index_pattern.test.ts similarity index 100% rename from public/app/plugins/datasource/elasticsearch/specs/index_pattern.jest.ts rename to public/app/plugins/datasource/elasticsearch/specs/index_pattern.test.ts diff --git a/public/app/plugins/datasource/elasticsearch/specs/query_builder.jest.ts b/public/app/plugins/datasource/elasticsearch/specs/query_builder.test.ts similarity index 100% rename from public/app/plugins/datasource/elasticsearch/specs/query_builder.jest.ts rename to public/app/plugins/datasource/elasticsearch/specs/query_builder.test.ts diff --git a/public/app/plugins/datasource/elasticsearch/specs/query_def.jest.ts b/public/app/plugins/datasource/elasticsearch/specs/query_def.test.ts similarity index 100% rename from public/app/plugins/datasource/elasticsearch/specs/query_def.jest.ts rename to public/app/plugins/datasource/elasticsearch/specs/query_def.test.ts diff --git a/public/app/plugins/datasource/graphite/specs/datasource.jest.ts b/public/app/plugins/datasource/graphite/specs/datasource.test.ts similarity index 100% rename from public/app/plugins/datasource/graphite/specs/datasource.jest.ts rename to public/app/plugins/datasource/graphite/specs/datasource.test.ts diff --git a/public/app/plugins/datasource/graphite/specs/gfunc.jest.ts b/public/app/plugins/datasource/graphite/specs/gfunc.test.ts similarity index 100% rename from public/app/plugins/datasource/graphite/specs/gfunc.jest.ts rename to public/app/plugins/datasource/graphite/specs/gfunc.test.ts diff --git a/public/app/plugins/datasource/graphite/specs/graphite_query.jest.ts b/public/app/plugins/datasource/graphite/specs/graphite_query.test.ts similarity index 100% rename from public/app/plugins/datasource/graphite/specs/graphite_query.jest.ts rename to public/app/plugins/datasource/graphite/specs/graphite_query.test.ts diff --git a/public/app/plugins/datasource/graphite/specs/lexer.jest.ts b/public/app/plugins/datasource/graphite/specs/lexer.test.ts similarity index 100% rename from public/app/plugins/datasource/graphite/specs/lexer.jest.ts rename to public/app/plugins/datasource/graphite/specs/lexer.test.ts diff --git a/public/app/plugins/datasource/graphite/specs/parser.jest.ts b/public/app/plugins/datasource/graphite/specs/parser.test.ts similarity index 100% rename from public/app/plugins/datasource/graphite/specs/parser.jest.ts rename to public/app/plugins/datasource/graphite/specs/parser.test.ts diff --git a/public/app/plugins/datasource/graphite/specs/query_ctrl.jest.ts b/public/app/plugins/datasource/graphite/specs/query_ctrl.test.ts similarity index 100% rename from public/app/plugins/datasource/graphite/specs/query_ctrl.jest.ts rename to public/app/plugins/datasource/graphite/specs/query_ctrl.test.ts diff --git a/public/app/plugins/datasource/influxdb/specs/influx_query.jest.ts b/public/app/plugins/datasource/influxdb/specs/influx_query.test.ts similarity index 100% rename from public/app/plugins/datasource/influxdb/specs/influx_query.jest.ts rename to public/app/plugins/datasource/influxdb/specs/influx_query.test.ts diff --git a/public/app/plugins/datasource/influxdb/specs/influx_series.jest.ts b/public/app/plugins/datasource/influxdb/specs/influx_series.test.ts similarity index 100% rename from public/app/plugins/datasource/influxdb/specs/influx_series.jest.ts rename to public/app/plugins/datasource/influxdb/specs/influx_series.test.ts diff --git a/public/app/plugins/datasource/influxdb/specs/query_builder.jest.ts b/public/app/plugins/datasource/influxdb/specs/query_builder.test.ts similarity index 100% rename from public/app/plugins/datasource/influxdb/specs/query_builder.jest.ts rename to public/app/plugins/datasource/influxdb/specs/query_builder.test.ts diff --git a/public/app/plugins/datasource/influxdb/specs/query_ctrl.jest.ts b/public/app/plugins/datasource/influxdb/specs/query_ctrl.test.ts similarity index 100% rename from public/app/plugins/datasource/influxdb/specs/query_ctrl.jest.ts rename to public/app/plugins/datasource/influxdb/specs/query_ctrl.test.ts diff --git a/public/app/plugins/datasource/influxdb/specs/query_part.jest.ts b/public/app/plugins/datasource/influxdb/specs/query_part.test.ts similarity index 100% rename from public/app/plugins/datasource/influxdb/specs/query_part.jest.ts rename to public/app/plugins/datasource/influxdb/specs/query_part.test.ts diff --git a/public/app/plugins/datasource/influxdb/specs/response_parser.jest.ts b/public/app/plugins/datasource/influxdb/specs/response_parser.test.ts similarity index 100% rename from public/app/plugins/datasource/influxdb/specs/response_parser.jest.ts rename to public/app/plugins/datasource/influxdb/specs/response_parser.test.ts diff --git a/public/app/plugins/datasource/logging/datasource.jest.ts b/public/app/plugins/datasource/logging/datasource.test.ts similarity index 100% rename from public/app/plugins/datasource/logging/datasource.jest.ts rename to public/app/plugins/datasource/logging/datasource.test.ts diff --git a/public/app/plugins/datasource/logging/result_transformer.jest.ts b/public/app/plugins/datasource/logging/result_transformer.test.ts similarity index 100% rename from public/app/plugins/datasource/logging/result_transformer.jest.ts rename to public/app/plugins/datasource/logging/result_transformer.test.ts diff --git a/public/app/plugins/datasource/mssql/specs/datasource.jest.ts b/public/app/plugins/datasource/mssql/specs/datasource.test.ts similarity index 100% rename from public/app/plugins/datasource/mssql/specs/datasource.jest.ts rename to public/app/plugins/datasource/mssql/specs/datasource.test.ts diff --git a/public/app/plugins/datasource/mysql/specs/datasource.jest.ts b/public/app/plugins/datasource/mysql/specs/datasource.test.ts similarity index 100% rename from public/app/plugins/datasource/mysql/specs/datasource.jest.ts rename to public/app/plugins/datasource/mysql/specs/datasource.test.ts diff --git a/public/app/plugins/datasource/opentsdb/specs/datasource.jest.ts b/public/app/plugins/datasource/opentsdb/specs/datasource.test.ts similarity index 100% rename from public/app/plugins/datasource/opentsdb/specs/datasource.jest.ts rename to public/app/plugins/datasource/opentsdb/specs/datasource.test.ts diff --git a/public/app/plugins/datasource/opentsdb/specs/query_ctrl.jest.ts b/public/app/plugins/datasource/opentsdb/specs/query_ctrl.test.ts similarity index 100% rename from public/app/plugins/datasource/opentsdb/specs/query_ctrl.jest.ts rename to public/app/plugins/datasource/opentsdb/specs/query_ctrl.test.ts diff --git a/public/app/plugins/datasource/postgres/specs/datasource.jest.ts b/public/app/plugins/datasource/postgres/specs/datasource.test.ts similarity index 100% rename from public/app/plugins/datasource/postgres/specs/datasource.jest.ts rename to public/app/plugins/datasource/postgres/specs/datasource.test.ts diff --git a/public/app/plugins/datasource/prometheus/specs/completer.jest.ts b/public/app/plugins/datasource/prometheus/specs/completer.test.ts similarity index 100% rename from public/app/plugins/datasource/prometheus/specs/completer.jest.ts rename to public/app/plugins/datasource/prometheus/specs/completer.test.ts diff --git a/public/app/plugins/datasource/prometheus/specs/datasource.jest.ts b/public/app/plugins/datasource/prometheus/specs/datasource.test.ts similarity index 100% rename from public/app/plugins/datasource/prometheus/specs/datasource.jest.ts rename to public/app/plugins/datasource/prometheus/specs/datasource.test.ts diff --git a/public/app/plugins/datasource/prometheus/specs/metric_find_query.jest.ts b/public/app/plugins/datasource/prometheus/specs/metric_find_query.test.ts similarity index 100% rename from public/app/plugins/datasource/prometheus/specs/metric_find_query.jest.ts rename to public/app/plugins/datasource/prometheus/specs/metric_find_query.test.ts diff --git a/public/app/plugins/datasource/prometheus/specs/result_transformer.jest.ts b/public/app/plugins/datasource/prometheus/specs/result_transformer.test.ts similarity index 100% rename from public/app/plugins/datasource/prometheus/specs/result_transformer.jest.ts rename to public/app/plugins/datasource/prometheus/specs/result_transformer.test.ts diff --git a/public/app/plugins/panel/graph/specs/align_yaxes.jest.ts b/public/app/plugins/panel/graph/specs/align_yaxes.test.ts similarity index 100% rename from public/app/plugins/panel/graph/specs/align_yaxes.jest.ts rename to public/app/plugins/panel/graph/specs/align_yaxes.test.ts diff --git a/public/app/plugins/panel/graph/specs/data_processor.jest.ts b/public/app/plugins/panel/graph/specs/data_processor.test.ts similarity index 100% rename from public/app/plugins/panel/graph/specs/data_processor.jest.ts rename to public/app/plugins/panel/graph/specs/data_processor.test.ts diff --git a/public/app/plugins/panel/graph/specs/graph.jest.ts b/public/app/plugins/panel/graph/specs/graph.test.ts similarity index 100% rename from public/app/plugins/panel/graph/specs/graph.jest.ts rename to public/app/plugins/panel/graph/specs/graph.test.ts diff --git a/public/app/plugins/panel/graph/specs/graph_ctrl.jest.ts b/public/app/plugins/panel/graph/specs/graph_ctrl.test.ts similarity index 100% rename from public/app/plugins/panel/graph/specs/graph_ctrl.jest.ts rename to public/app/plugins/panel/graph/specs/graph_ctrl.test.ts diff --git a/public/app/plugins/panel/graph/specs/graph_tooltip.jest.ts b/public/app/plugins/panel/graph/specs/graph_tooltip.test.ts similarity index 100% rename from public/app/plugins/panel/graph/specs/graph_tooltip.jest.ts rename to public/app/plugins/panel/graph/specs/graph_tooltip.test.ts diff --git a/public/app/plugins/panel/graph/specs/histogram.jest.ts b/public/app/plugins/panel/graph/specs/histogram.test.ts similarity index 100% rename from public/app/plugins/panel/graph/specs/histogram.jest.ts rename to public/app/plugins/panel/graph/specs/histogram.test.ts diff --git a/public/app/plugins/panel/graph/specs/series_override_ctrl.jest.ts b/public/app/plugins/panel/graph/specs/series_override_ctrl.test.ts similarity index 100% rename from public/app/plugins/panel/graph/specs/series_override_ctrl.jest.ts rename to public/app/plugins/panel/graph/specs/series_override_ctrl.test.ts diff --git a/public/app/plugins/panel/graph/specs/threshold_manager.jest.ts b/public/app/plugins/panel/graph/specs/threshold_manager.test.ts similarity index 100% rename from public/app/plugins/panel/graph/specs/threshold_manager.jest.ts rename to public/app/plugins/panel/graph/specs/threshold_manager.test.ts diff --git a/public/app/plugins/panel/heatmap/specs/heatmap_ctrl.jest.ts b/public/app/plugins/panel/heatmap/specs/heatmap_ctrl.test.ts similarity index 100% rename from public/app/plugins/panel/heatmap/specs/heatmap_ctrl.jest.ts rename to public/app/plugins/panel/heatmap/specs/heatmap_ctrl.test.ts diff --git a/public/app/plugins/panel/heatmap/specs/heatmap_data_converter.jest.ts b/public/app/plugins/panel/heatmap/specs/heatmap_data_converter.test.ts similarity index 100% rename from public/app/plugins/panel/heatmap/specs/heatmap_data_converter.jest.ts rename to public/app/plugins/panel/heatmap/specs/heatmap_data_converter.test.ts diff --git a/public/app/plugins/panel/singlestat/specs/singlestat.jest.ts b/public/app/plugins/panel/singlestat/specs/singlestat.test.ts similarity index 100% rename from public/app/plugins/panel/singlestat/specs/singlestat.jest.ts rename to public/app/plugins/panel/singlestat/specs/singlestat.test.ts diff --git a/public/app/plugins/panel/singlestat/specs/singlestat_panel.jest.ts b/public/app/plugins/panel/singlestat/specs/singlestat_panel.test.ts similarity index 100% rename from public/app/plugins/panel/singlestat/specs/singlestat_panel.jest.ts rename to public/app/plugins/panel/singlestat/specs/singlestat_panel.test.ts diff --git a/public/app/plugins/panel/table/specs/renderer.jest.ts b/public/app/plugins/panel/table/specs/renderer.test.ts similarity index 100% rename from public/app/plugins/panel/table/specs/renderer.jest.ts rename to public/app/plugins/panel/table/specs/renderer.test.ts diff --git a/public/app/plugins/panel/table/specs/transformers.jest.ts b/public/app/plugins/panel/table/specs/transformers.test.ts similarity index 100% rename from public/app/plugins/panel/table/specs/transformers.jest.ts rename to public/app/plugins/panel/table/specs/transformers.test.ts diff --git a/public/app/stores/AlertListStore/AlertListStore.jest.ts b/public/app/stores/AlertListStore/AlertListStore.test.ts similarity index 100% rename from public/app/stores/AlertListStore/AlertListStore.jest.ts rename to public/app/stores/AlertListStore/AlertListStore.test.ts diff --git a/public/app/stores/NavStore/NavStore.jest.ts b/public/app/stores/NavStore/NavStore.test.ts similarity index 100% rename from public/app/stores/NavStore/NavStore.jest.ts rename to public/app/stores/NavStore/NavStore.test.ts diff --git a/public/app/stores/PermissionsStore/PermissionsStore.jest.ts b/public/app/stores/PermissionsStore/PermissionsStore.test.ts similarity index 100% rename from public/app/stores/PermissionsStore/PermissionsStore.jest.ts rename to public/app/stores/PermissionsStore/PermissionsStore.test.ts diff --git a/public/app/stores/ViewStore/ViewStore.jest.ts b/public/app/stores/ViewStore/ViewStore.test.ts similarity index 100% rename from public/app/stores/ViewStore/ViewStore.jest.ts rename to public/app/stores/ViewStore/ViewStore.test.ts diff --git a/public/test/core/utils/version_jest.ts b/public/test/core/utils/version_test.ts similarity index 100% rename from public/test/core/utils/version_jest.ts rename to public/test/core/utils/version_test.ts From 86a27895415fa28b8850852fdbfb4ab3ff793dca Mon Sep 17 00:00:00 2001 From: Tobias Skarhed Date: Tue, 14 Aug 2018 11:23:55 +0200 Subject: [PATCH 420/786] Remove dependencies --- yarn.lock | 549 ++++-------------------------------------------------- 1 file changed, 33 insertions(+), 516 deletions(-) diff --git a/yarn.lock b/yarn.lock index 89e74828351..c4bd6704839 100644 --- a/yarn.lock +++ b/yarn.lock @@ -422,13 +422,6 @@ abbrev@1, abbrev@~1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8" -accepts@1.3.3: - version "1.3.3" - resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.3.tgz#c3ca7434938648c3e0d9c1e328dd68b622c284ca" - dependencies: - mime-types "~2.1.11" - negotiator "0.6.1" - accepts@~1.3.4, accepts@~1.3.5: version "1.3.5" resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.5.tgz#eb777df6011723a3b14e8a72c0805c8e86746bd2" @@ -480,10 +473,6 @@ add-dom-event-listener@1.x: dependencies: object-assign "4.x" -after@0.8.2: - version "0.8.2" - resolved "https://registry.yarnpkg.com/after/-/after-0.8.2.tgz#fedb394f9f0e02aa9768e702bda23b505fae7e1f" - agent-base@4, agent-base@^4.1.0, agent-base@~4.2.0: version "4.2.0" resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-4.2.0.tgz#9838b5c3392b962bad031e6a4c5e1024abec45ce" @@ -769,10 +758,6 @@ array-reduce@~0.0.0: version "0.0.0" resolved "https://registry.yarnpkg.com/array-reduce/-/array-reduce-0.0.0.tgz#173899d3ffd1c7d9383e4479525dbe278cab5f2b" -array-slice@^0.2.3: - version "0.2.3" - resolved "https://registry.yarnpkg.com/array-slice/-/array-slice-0.2.3.tgz#dd3cfb80ed7973a75117cdac69b0b99ec86186f5" - array-tree-filter@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/array-tree-filter/-/array-tree-filter-1.0.1.tgz#0a8ad1eefd38ce88858632f9cc0423d7634e4d5d" @@ -795,10 +780,6 @@ array-unique@^0.3.2: version "0.3.2" resolved "https://registry.yarnpkg.com/array-unique/-/array-unique-0.3.2.tgz#a894b75d4bc4f6cd679ef3244a9fd8f46ae2d428" -arraybuffer.slice@0.0.6: - version "0.0.6" - resolved "https://registry.yarnpkg.com/arraybuffer.slice/-/arraybuffer.slice-0.0.6.tgz#f33b2159f0532a3f3107a272c0ccfbd1ad2979ca" - arrify@^1.0.0, arrify@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/arrify/-/arrify-1.0.1.tgz#898508da2226f380df904728456849c1501a4b0d" @@ -1520,7 +1501,7 @@ babel-register@^6.26.0, babel-register@^6.9.0: mkdirp "^0.5.1" source-map-support "^0.4.15" -babel-runtime@6.x, babel-runtime@^6.0.0, babel-runtime@^6.18.0, babel-runtime@^6.22.0, babel-runtime@^6.26.0, babel-runtime@^6.9.2: +babel-runtime@6.x, babel-runtime@^6.18.0, babel-runtime@^6.22.0, babel-runtime@^6.26.0, babel-runtime@^6.9.2: version "6.26.0" resolved "https://registry.yarnpkg.com/babel-runtime/-/babel-runtime-6.26.0.tgz#965c7058668e82b55d7bfe04ff2337bc8b5647fe" dependencies: @@ -1568,10 +1549,6 @@ babylon@^7.0.0-beta.47: version "7.0.0-beta.47" resolved "https://registry.yarnpkg.com/babylon/-/babylon-7.0.0-beta.47.tgz#6d1fa44f0abec41ab7c780481e62fd9aafbdea80" -backo2@1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/backo2/-/backo2-1.0.2.tgz#31ab1ac8b129363463e35b3ebb69f4dfcfba7947" - balanced-match@^0.4.2: version "0.4.2" resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-0.4.2.tgz#cb3f3e3c732dc0f01ee70b403f302e61d7709838" @@ -1584,18 +1561,10 @@ baron@^3.0.3: version "3.0.3" resolved "https://registry.yarnpkg.com/baron/-/baron-3.0.3.tgz#0f0a08a567062882e130a0ecfd41a46d52103f4a" -base64-arraybuffer@0.1.5: - version "0.1.5" - resolved "https://registry.yarnpkg.com/base64-arraybuffer/-/base64-arraybuffer-0.1.5.tgz#73926771923b5a19747ad666aa5cd4bf9c6e9ce8" - base64-js@^1.0.2: version "1.3.0" resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.3.0.tgz#cab1e6118f051095e58b5281aea8c1cd22bfc0e3" -base64id@1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/base64id/-/base64id-1.0.0.tgz#47688cb99bb6804f0e06d3e763b1c32e57d8e6b6" - base@^0.11.1: version "0.11.2" resolved "https://registry.yarnpkg.com/base/-/base-0.11.2.tgz#7bde5ced145b6d551a90db87f83c558b4eb48a8f" @@ -1622,12 +1591,6 @@ bcrypt-pbkdf@^1.0.0: dependencies: tweetnacl "^0.14.3" -better-assert@~1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/better-assert/-/better-assert-1.0.2.tgz#40866b9e1b9e0b55b481894311e68faffaebc522" - dependencies: - callsite "1.0.0" - bfj-node4@^5.2.0: version "5.3.1" resolved "https://registry.yarnpkg.com/bfj-node4/-/bfj-node4-5.3.1.tgz#e23d8b27057f1d0214fc561142ad9db998f26830" @@ -1665,17 +1628,13 @@ bl@^1.0.0: readable-stream "^2.3.5" safe-buffer "^5.1.1" -blob@0.0.4: - version "0.0.4" - resolved "https://registry.yarnpkg.com/blob/-/blob-0.0.4.tgz#bcf13052ca54463f30f9fc7e95b9a47630a94921" - block-stream@*: version "0.0.9" resolved "https://registry.yarnpkg.com/block-stream/-/block-stream-0.0.9.tgz#13ebfe778a03205cfe03751481ebb4b3300c126a" dependencies: inherits "~2.0.0" -bluebird@^3.3.0, bluebird@^3.5.0, bluebird@^3.5.1, bluebird@~3.5.1: +bluebird@^3.5.0, bluebird@^3.5.1, bluebird@~3.5.1: version "3.5.1" resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.5.1.tgz#d9551f9de98f1fcda1e683d17ee91a0602ee2eb9" @@ -1698,21 +1657,6 @@ body-parser@1.18.2: raw-body "2.3.2" type-is "~1.6.15" -body-parser@^1.16.1: - version "1.18.3" - resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.18.3.tgz#5b292198ffdd553b3a0f20ded0592b956955c8b4" - dependencies: - bytes "3.0.0" - content-type "~1.0.4" - debug "2.6.9" - depd "~1.1.2" - http-errors "~1.6.3" - iconv-lite "0.4.23" - on-finished "~2.3.0" - qs "6.5.2" - raw-body "2.3.3" - type-is "~1.6.16" - bonjour@^3.5.0: version "3.5.0" resolved "https://registry.yarnpkg.com/bonjour/-/bonjour-3.5.0.tgz#8e890a183d8ee9a2393b3844c691a42bcf7bc9f5" @@ -1759,12 +1703,6 @@ brace@^0.10.0: dependencies: w3c-blob "0.0.1" -braces@^0.1.2: - version "0.1.5" - resolved "https://registry.yarnpkg.com/braces/-/braces-0.1.5.tgz#c085711085291d8b75fdd74eab0f8597280711e6" - dependencies: - expand-range "^0.1.0" - braces@^1.8.2: version "1.8.5" resolved "https://registry.yarnpkg.com/braces/-/braces-1.8.5.tgz#ba77962e12dff969d6b76711e914b737857bf6a7" @@ -2021,10 +1959,6 @@ caller-path@^0.1.0: dependencies: callsites "^0.2.0" -callsite@1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/callsite/-/callsite-1.0.0.tgz#280398e5d664bd74038b6f0905153e6e8af1bc20" - callsites@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/callsites/-/callsites-0.2.0.tgz#afab96262910a7f33c19a5775825c69f34e350ca" @@ -2169,7 +2103,7 @@ cheerio@^1.0.0-rc.2: lodash "^4.15.0" parse5 "^3.0.1" -chokidar@^1.4.1, chokidar@^1.6.0, chokidar@^1.7.0: +chokidar@^1.6.0, chokidar@^1.7.0: version "1.7.0" resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-1.7.0.tgz#798e689778151c8076b4b360e5edd28cda2bb468" dependencies: @@ -2479,7 +2413,7 @@ colors@1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/colors/-/colors-1.0.3.tgz#0433f44d809680fdeb60ed260f1b0c262e82a40b" -colors@^1.1.0, colors@^1.1.2: +colors@^1.1.2: version "1.3.0" resolved "https://registry.yarnpkg.com/colors/-/colors-1.3.0.tgz#5f20c9fef6945cb1134260aab33bfbdc8295e04e" @@ -2494,12 +2428,6 @@ columnify@~1.5.4: strip-ansi "^3.0.0" wcwidth "^1.0.0" -combine-lists@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/combine-lists/-/combine-lists-1.0.1.tgz#458c07e09e0d900fc28b70a3fec2dacd1d2cb7f6" - dependencies: - lodash "^4.5.0" - combined-stream@1.0.6, combined-stream@^1.0.5, combined-stream@~1.0.5: version "1.0.6" resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.6.tgz#723e7df6e801ac5613113a7e445a9b69cb632818" @@ -2538,21 +2466,13 @@ compare-versions@^3.1.0: version "3.2.1" resolved "https://registry.yarnpkg.com/compare-versions/-/compare-versions-3.2.1.tgz#a49eb7689d4caaf0b6db5220173fd279614000f7" -component-bind@1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/component-bind/-/component-bind-1.0.0.tgz#00c608ab7dcd93897c0009651b1d3a8e1e73bbd1" - component-classes@^1.2.5: version "1.2.6" resolved "https://registry.yarnpkg.com/component-classes/-/component-classes-1.2.6.tgz#c642394c3618a4d8b0b8919efccbbd930e5cd691" dependencies: component-indexof "0.0.3" -component-emitter@1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.1.2.tgz#296594f2753daa63996d2af08d15a95116c9aec3" - -component-emitter@1.2.1, component-emitter@^1.2.1: +component-emitter@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.2.1.tgz#137918d6d78283f7df7a6b7c5a63e140e69425e6" @@ -2560,10 +2480,6 @@ component-indexof@0.0.3: version "0.0.3" resolved "https://registry.yarnpkg.com/component-indexof/-/component-indexof-0.0.3.tgz#11d091312239eb8f32c8f25ae9cb002ffe8d3c24" -component-inherit@0.0.3: - version "0.0.3" - resolved "https://registry.yarnpkg.com/component-inherit/-/component-inherit-0.0.3.tgz#645fc4adf58b72b649d5cae65135619db26ff143" - compress-commons@^1.2.0: version "1.2.2" resolved "https://registry.yarnpkg.com/compress-commons/-/compress-commons-1.2.2.tgz#524a9f10903f3a813389b0225d27c48bb751890f" @@ -2626,15 +2542,6 @@ connect-history-api-fallback@^1.3.0: version "1.5.0" resolved "https://registry.yarnpkg.com/connect-history-api-fallback/-/connect-history-api-fallback-1.5.0.tgz#b06873934bc5e344fef611a196a6faae0aee015a" -connect@^3.6.0: - version "3.6.6" - resolved "https://registry.yarnpkg.com/connect/-/connect-3.6.6.tgz#09eff6c55af7236e137135a72574858b6786f524" - dependencies: - debug "2.6.9" - finalhandler "1.1.0" - parseurl "~1.3.2" - utils-merge "1.0.1" - console-browserify@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/console-browserify/-/console-browserify-1.1.0.tgz#f0241c45730a9fc6323b206dbf38edc741d0bb10" @@ -2699,7 +2606,7 @@ core-js@^1.0.0: version "1.2.7" resolved "https://registry.yarnpkg.com/core-js/-/core-js-1.2.7.tgz#652294c14651db28fa93bd2d5ff2983a4f08c636" -core-js@^2.0.0, core-js@^2.2.0, core-js@^2.4.0, core-js@^2.4.1, core-js@^2.5.0: +core-js@^2.0.0, core-js@^2.4.0, core-js@^2.4.1, core-js@^2.5.0: version "2.5.7" resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.5.7.tgz#f972608ff0cead68b841a16a932d0b183791814e" @@ -2959,10 +2866,6 @@ currently-unhandled@^0.4.1: dependencies: array-find-index "^1.0.1" -custom-event@~1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/custom-event/-/custom-event-1.0.1.tgz#5d02a46850adf1b4a317946a3928fccb5bfd0425" - cyclist@~0.2.2: version "0.2.2" resolved "https://registry.yarnpkg.com/cyclist/-/cyclist-0.2.2.tgz#1b33792e11e914a2fd6d6ed6447464444e5fa640" @@ -3241,18 +3144,6 @@ dateformat@~1.0.12: get-stdin "^4.0.1" meow "^3.3.0" -debug@2.2.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/debug/-/debug-2.2.0.tgz#f87057e995b1a1f6ae6a4960664137bc56f039da" - dependencies: - ms "0.7.1" - -debug@2.3.3: - version "2.3.3" - resolved "https://registry.yarnpkg.com/debug/-/debug-2.3.3.tgz#40c453e67e6e13c901ddec317af8986cda9eff8c" - dependencies: - ms "0.7.2" - debug@2.6.9, debug@^2.1.1, debug@^2.1.2, debug@^2.1.3, debug@^2.2.0, debug@^2.3.3, debug@^2.6.6, debug@^2.6.8, debug@^2.6.9: version "2.6.9" resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" @@ -3442,10 +3333,6 @@ dezalgo@^1.0.0, dezalgo@~1.0.3: asap "^2.0.0" wrappy "1" -di@^0.0.1: - version "0.0.1" - resolved "https://registry.yarnpkg.com/di/-/di-0.0.1.tgz#806649326ceaa7caa3306d75d985ea2748ba913c" - diff-match-patch@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/diff-match-patch/-/diff-match-patch-1.0.1.tgz#d5f880213d82fbc124d2b95111fb3c033dbad7fa" @@ -3523,15 +3410,6 @@ dom-helpers@^3.3.1: version "3.3.1" resolved "https://registry.yarnpkg.com/dom-helpers/-/dom-helpers-3.3.1.tgz#fc1a4e15ffdf60ddde03a480a9c0fece821dd4a6" -dom-serialize@^2.2.0: - version "2.2.1" - resolved "https://registry.yarnpkg.com/dom-serialize/-/dom-serialize-2.2.1.tgz#562ae8999f44be5ea3076f5419dcd59eb43ac95b" - dependencies: - custom-event "~1.0.0" - ent "~2.2.0" - extend "^3.0.0" - void-elements "^2.0.0" - dom-serializer@0, dom-serializer@~0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-0.1.0.tgz#073c697546ce0780ce23be4a28e293e40bc30c82" @@ -3703,7 +3581,7 @@ empower@^1.2.3: core-js "^2.0.0" empower-core "^0.6.2" -encodeurl@~1.0.1, encodeurl@~1.0.2: +encodeurl@~1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" @@ -3719,45 +3597,6 @@ end-of-stream@^1.0.0, end-of-stream@^1.1.0: dependencies: once "^1.4.0" -engine.io-client@1.8.3: - version "1.8.3" - resolved "https://registry.yarnpkg.com/engine.io-client/-/engine.io-client-1.8.3.tgz#1798ed93451246453d4c6f635d7a201fe940d5ab" - dependencies: - component-emitter "1.2.1" - component-inherit "0.0.3" - debug "2.3.3" - engine.io-parser "1.3.2" - has-cors "1.1.0" - indexof "0.0.1" - parsejson "0.0.3" - parseqs "0.0.5" - parseuri "0.0.5" - ws "1.1.2" - xmlhttprequest-ssl "1.5.3" - yeast "0.1.2" - -engine.io-parser@1.3.2: - version "1.3.2" - resolved "https://registry.yarnpkg.com/engine.io-parser/-/engine.io-parser-1.3.2.tgz#937b079f0007d0893ec56d46cb220b8cb435220a" - dependencies: - after "0.8.2" - arraybuffer.slice "0.0.6" - base64-arraybuffer "0.1.5" - blob "0.0.4" - has-binary "0.1.7" - wtf-8 "1.0.0" - -engine.io@1.8.3: - version "1.8.3" - resolved "https://registry.yarnpkg.com/engine.io/-/engine.io-1.8.3.tgz#8de7f97895d20d39b85f88eeee777b2bd42b13d4" - dependencies: - accepts "1.3.3" - base64id "1.0.0" - cookie "0.3.1" - debug "2.3.3" - engine.io-parser "1.3.2" - ws "1.1.2" - enhanced-resolve@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-4.0.0.tgz#e34a6eaa790f62fccd71d93959f56b2b432db10a" @@ -3766,10 +3605,6 @@ enhanced-resolve@^4.0.0: memory-fs "^0.4.0" tapable "^1.0.0" -ent@~2.2.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/ent/-/ent-2.2.0.tgz#e964219325a21d05f44466a2f686ed6ce5f5dd1d" - entities@^1.1.1, entities@~1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/entities/-/entities-1.1.1.tgz#6e5c2d0a5621b5dadaecef80b90edfb5cd7772f0" @@ -4138,14 +3973,6 @@ exit@^0.1.2, exit@~0.1.1: version "0.1.2" resolved "https://registry.yarnpkg.com/exit/-/exit-0.1.2.tgz#0632638f8d877cc82107d30a0fff1a17cba1cd0c" -expand-braces@^0.1.1: - version "0.1.2" - resolved "https://registry.yarnpkg.com/expand-braces/-/expand-braces-0.1.2.tgz#488b1d1d2451cb3d3a6b192cfc030f44c5855fea" - dependencies: - array-slice "^0.2.3" - array-unique "^0.2.1" - braces "^0.1.2" - expand-brackets@^0.1.4: version "0.1.5" resolved "https://registry.yarnpkg.com/expand-brackets/-/expand-brackets-0.1.5.tgz#df07284e342a807cd733ac5af72411e581d1177b" @@ -4164,13 +3991,6 @@ expand-brackets@^2.1.4: snapdragon "^0.8.1" to-regex "^3.0.1" -expand-range@^0.1.0: - version "0.1.1" - resolved "https://registry.yarnpkg.com/expand-range/-/expand-range-0.1.1.tgz#4cb8eda0993ca56fa4f41fc42f3cbb4ccadff044" - dependencies: - is-number "^0.1.1" - repeat-string "^0.2.2" - expand-range@^1.8.1: version "1.8.2" resolved "https://registry.yarnpkg.com/expand-range/-/expand-range-1.8.2.tgz#a299effd335fe2721ebae8e257ec79644fc85337" @@ -4187,10 +4007,6 @@ expand-tilde@^2.0.0, expand-tilde@^2.0.2: dependencies: homedir-polyfill "^1.0.1" -expect.js@^0.3.1: - version "0.3.1" - resolved "https://registry.yarnpkg.com/expect.js/-/expect.js-0.3.1.tgz#b0a59a0d2eff5437544ebf0ceaa6015841d09b5b" - expect.js@~0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/expect.js/-/expect.js-0.2.0.tgz#1028533d2c1c363f74a6796ff57ec0520ded2be1" @@ -4258,7 +4074,7 @@ extend-shallow@^3.0.0, extend-shallow@^3.0.2: assign-symbols "^1.0.0" is-extendable "^1.0.1" -extend@^3.0.0, extend@~3.0.0, extend@~3.0.1: +extend@~3.0.0, extend@~3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.1.tgz#a755ea7bc1adfcc5a31ce7e762dbaadc5e636444" @@ -4455,18 +4271,6 @@ fill-range@^4.0.0: repeat-string "^1.6.1" to-regex-range "^2.1.0" -finalhandler@1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.1.0.tgz#ce0b6855b45853e791b2fcc680046d88253dd7f5" - dependencies: - debug "2.6.9" - encodeurl "~1.0.1" - escape-html "~1.0.3" - on-finished "~2.3.0" - parseurl "~1.3.2" - statuses "~1.3.1" - unpipe "~1.0.0" - finalhandler@1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.1.1.tgz#eebf4ed840079c83f4249038c9d703008301b105" @@ -4654,12 +4458,6 @@ front-matter@2.1.2: dependencies: js-yaml "^3.4.6" -fs-access@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/fs-access/-/fs-access-1.0.1.tgz#d6a87f262271cefebec30c553407fb995da8777a" - dependencies: - null-check "^1.0.0" - fs-constants@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/fs-constants/-/fs-constants-1.0.0.tgz#6be0de9be998ce16af8afc24497b9ee9b7ccd9ad" @@ -5152,12 +4950,6 @@ grunt-exec@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/grunt-exec/-/grunt-exec-1.0.1.tgz#e5d53a39c5f346901305edee5c87db0f2af999c4" -grunt-karma@~2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/grunt-karma/-/grunt-karma-2.0.0.tgz#753583d115dfdc055fe57e58f96d6b3c7e612118" - dependencies: - lodash "^3.10.1" - grunt-known-options@~1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/grunt-known-options/-/grunt-known-options-1.1.0.tgz#a4274eeb32fa765da5a7a3b1712617ce3b144149" @@ -5311,20 +5103,10 @@ has-ansi@^2.0.0: dependencies: ansi-regex "^2.0.0" -has-binary@0.1.7: - version "0.1.7" - resolved "https://registry.yarnpkg.com/has-binary/-/has-binary-0.1.7.tgz#68e61eb16210c9545a0a5cce06a873912fe1e68c" - dependencies: - isarray "0.0.1" - has-color@~0.1.0: version "0.1.7" resolved "https://registry.yarnpkg.com/has-color/-/has-color-0.1.7.tgz#67144a5260c34fc3cca677d041daf52fe7b78b2f" -has-cors@1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/has-cors/-/has-cors-1.1.0.tgz#5e474793f7ea9843d1bb99c23eef49ff126fff39" - has-flag@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-1.0.0.tgz#9d9e793165ce017a00f00418c43f942a7b1d11fa" @@ -5583,7 +5365,7 @@ http-errors@1.6.2: setprototypeof "1.0.3" statuses ">= 1.3.1 < 2" -http-errors@1.6.3, http-errors@~1.6.2, http-errors@~1.6.3: +http-errors@~1.6.2: version "1.6.3" resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.6.3.tgz#8b55680bb4be283a0b5bf4ea2e38580be1d9320d" dependencies: @@ -5612,7 +5394,7 @@ http-proxy-middleware@~0.18.0: lodash "^4.17.5" micromatch "^3.1.9" -http-proxy@^1.13.0, http-proxy@^1.16.2: +http-proxy@^1.16.2: version "1.17.0" resolved "https://registry.yarnpkg.com/http-proxy/-/http-proxy-1.17.0.tgz#7ad38494658f84605e2f6db4436df410f4e5be9a" dependencies: @@ -5661,7 +5443,7 @@ husky@^0.14.3: normalize-path "^1.0.0" strip-indent "^2.0.0" -iconv-lite@0.4, iconv-lite@0.4.23, iconv-lite@^0.4.17, iconv-lite@^0.4.4, iconv-lite@~0.4.13: +iconv-lite@0.4, iconv-lite@^0.4.17, iconv-lite@^0.4.4, iconv-lite@~0.4.13: version "0.4.23" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.23.tgz#297871f63be507adcfbfca715d0cd0eed84e9a63" dependencies: @@ -6057,10 +5839,6 @@ is-number-object@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/is-number-object/-/is-number-object-1.0.3.tgz#f265ab89a9f445034ef6aff15a8f00b00f551799" -is-number@^0.1.1: - version "0.1.1" - resolved "https://registry.yarnpkg.com/is-number/-/is-number-0.1.1.tgz#69a7af116963d47206ec9bd9b48a14216f1e3806" - is-number@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/is-number/-/is-number-2.1.0.tgz#01fcbbb393463a548f2f466cce16dece49db908f" @@ -6229,7 +6007,7 @@ isarray@1.0.0, isarray@^1.0.0, isarray@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" -isbinaryfile@^3.0.0, isbinaryfile@^3.0.2: +isbinaryfile@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/isbinaryfile/-/isbinaryfile-3.0.2.tgz#4a3e974ec0cba9004d3fc6cde7209ea69368a621" @@ -6781,7 +6559,7 @@ json-stringify-safe@~5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" -json3@3.3.2, json3@^3.3.2: +json3@^3.3.2: version "3.3.2" resolved "https://registry.yarnpkg.com/json3/-/json3-3.3.2.tgz#3c0434743df93e2f5c42aee7b19bcb483575f4e1" @@ -6828,85 +6606,6 @@ jsprim@^1.2.2: json-schema "0.2.3" verror "1.10.0" -karma-chrome-launcher@~2.2.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/karma-chrome-launcher/-/karma-chrome-launcher-2.2.0.tgz#cf1b9d07136cc18fe239327d24654c3dbc368acf" - dependencies: - fs-access "^1.0.0" - which "^1.2.1" - -karma-expect@~1.1.3: - version "1.1.3" - resolved "https://registry.yarnpkg.com/karma-expect/-/karma-expect-1.1.3.tgz#c6b0a56ff18903db11af4f098cc6e7cf198ce275" - dependencies: - expect.js "^0.3.1" - -karma-mocha@~1.3.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/karma-mocha/-/karma-mocha-1.3.0.tgz#eeaac7ffc0e201eb63c467440d2b69c7cf3778bf" - dependencies: - minimist "1.2.0" - -karma-phantomjs-launcher@1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/karma-phantomjs-launcher/-/karma-phantomjs-launcher-1.0.4.tgz#d23ca34801bda9863ad318e3bb4bd4062b13acd2" - dependencies: - lodash "^4.0.1" - phantomjs-prebuilt "^2.1.7" - -karma-sinon@^1.0.5: - version "1.0.5" - resolved "https://registry.yarnpkg.com/karma-sinon/-/karma-sinon-1.0.5.tgz#4e3443f2830fdecff624d3747163f1217daa2a9a" - -karma-sourcemap-loader@^0.3.7: - version "0.3.7" - resolved "https://registry.yarnpkg.com/karma-sourcemap-loader/-/karma-sourcemap-loader-0.3.7.tgz#91322c77f8f13d46fed062b042e1009d4c4505d8" - dependencies: - graceful-fs "^4.1.2" - -karma-webpack@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/karma-webpack/-/karma-webpack-3.0.0.tgz#bf009c5b73c667c11c015717e9e520f581317c44" - dependencies: - async "^2.0.0" - babel-runtime "^6.0.0" - loader-utils "^1.0.0" - lodash "^4.0.0" - source-map "^0.5.6" - webpack-dev-middleware "^2.0.6" - -karma@1.7.0: - version "1.7.0" - resolved "https://registry.yarnpkg.com/karma/-/karma-1.7.0.tgz#6f7a1a406446fa2e187ec95398698f4cee476269" - dependencies: - bluebird "^3.3.0" - body-parser "^1.16.1" - chokidar "^1.4.1" - colors "^1.1.0" - combine-lists "^1.0.0" - connect "^3.6.0" - core-js "^2.2.0" - di "^0.0.1" - dom-serialize "^2.2.0" - expand-braces "^0.1.1" - glob "^7.1.1" - graceful-fs "^4.1.2" - http-proxy "^1.13.0" - isbinaryfile "^3.0.0" - lodash "^3.8.0" - log4js "^0.6.31" - mime "^1.3.4" - minimatch "^3.0.2" - optimist "^0.6.1" - qjobs "^1.1.4" - range-parser "^1.2.0" - rimraf "^2.6.0" - safe-buffer "^5.0.1" - socket.io "1.7.3" - source-map "^0.5.3" - tmp "0.0.31" - useragent "^2.1.12" - kew@^0.7.0: version "0.7.0" resolved "https://registry.yarnpkg.com/kew/-/kew-0.7.0.tgz#79d93d2d33363d6fdd2970b335d9141ad591d79b" @@ -7164,7 +6863,7 @@ loader-runner@^2.3.0: version "2.3.0" resolved "https://registry.yarnpkg.com/loader-runner/-/loader-runner-2.3.0.tgz#f482aea82d543e07921700d5a46ef26fdac6b8a2" -loader-utils@1.1.0, loader-utils@^1.0.0, loader-utils@^1.0.1, loader-utils@^1.0.2, loader-utils@^1.1.0: +loader-utils@1.1.0, loader-utils@^1.0.1, loader-utils@^1.0.2, loader-utils@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-1.1.0.tgz#c98aef488bcceda2ffb5e2de646d6a754429f5cd" dependencies: @@ -7320,11 +7019,11 @@ lodash.without@~4.4.0: version "4.4.0" resolved "https://registry.yarnpkg.com/lodash.without/-/lodash.without-4.4.0.tgz#3cd4574a00b67bae373a94b748772640507b7aac" -lodash@^3.10.1, lodash@^3.6.0, lodash@^3.8.0: +lodash@^3.10.1, lodash@^3.6.0: version "3.10.1" resolved "https://registry.yarnpkg.com/lodash/-/lodash-3.10.1.tgz#5bf45e8e49ba4189e17d482789dfd15bd140b7b6" -lodash@^4.0.0, lodash@^4.0.1, lodash@^4.1.1, lodash@^4.13.1, lodash@^4.15.0, lodash@^4.17.10, lodash@^4.17.2, lodash@^4.17.3, lodash@^4.17.4, lodash@^4.17.5, lodash@^4.3.0, lodash@^4.5.0, lodash@^4.7.0, lodash@^4.8.0, lodash@~4.17.10, lodash@~4.17.5: +lodash@^4.0.0, lodash@^4.1.1, lodash@^4.13.1, lodash@^4.15.0, lodash@^4.17.10, lodash@^4.17.2, lodash@^4.17.3, lodash@^4.17.4, lodash@^4.17.5, lodash@^4.3.0, lodash@^4.7.0, lodash@^4.8.0, lodash@~4.17.10, lodash@~4.17.5: version "4.17.10" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.10.tgz#1b7793cf7259ea38fb3661d4d38b3260af8ae4e7" @@ -7351,13 +7050,6 @@ log-update@^1.0.2: ansi-escapes "^1.0.0" cli-cursor "^1.0.2" -log4js@^0.6.31: - version "0.6.38" - resolved "https://registry.yarnpkg.com/log4js/-/log4js-0.6.38.tgz#2c494116695d6fb25480943d3fc872e662a522fd" - dependencies: - readable-stream "~1.0.2" - semver "~4.3.3" - loglevel@^1.4.1: version "1.6.1" resolved "https://registry.yarnpkg.com/loglevel/-/loglevel-1.6.1.tgz#e0fc95133b6ef276cdc8887cdaf24aa6f156f8fa" @@ -7412,7 +7104,7 @@ lowercase-keys@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-1.0.1.tgz#6f9e30b47084d971a7c820ff15a6c5167b74c26f" -lru-cache@4.1.x, lru-cache@^4.0.1, lru-cache@^4.1.1, lru-cache@^4.1.2, lru-cache@^4.1.3: +lru-cache@^4.0.1, lru-cache@^4.1.1, lru-cache@^4.1.2, lru-cache@^4.1.3: version "4.1.3" resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-4.1.3.tgz#a1175cf3496dfc8436c156c334b4955992bce69c" dependencies: @@ -7654,7 +7346,7 @@ mime-db@~1.33.0: version "1.33.0" resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.33.0.tgz#a3492050a5cb9b63450541e39d9788d2272783db" -mime-types@^2.1.12, mime-types@~2.1.11, mime-types@~2.1.17, mime-types@~2.1.18, mime-types@~2.1.7: +mime-types@^2.1.12, mime-types@~2.1.17, mime-types@~2.1.18, mime-types@~2.1.7: version "2.1.18" resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.18.tgz#6f323f60a83d11146f831ff11fd66e2fe5503bb8" dependencies: @@ -7664,10 +7356,6 @@ mime@1.4.1: version "1.4.1" resolved "https://registry.yarnpkg.com/mime/-/mime-1.4.1.tgz#121f9ebc49e3766f311a76e1fa1c8003c4b03aa6" -mime@^1.3.4: - version "1.6.0" - resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" - mime@^2.1.0: version "2.3.1" resolved "https://registry.yarnpkg.com/mime/-/mime-2.3.1.tgz#b1621c54d63b97c47d3cfe7f7215f7d64517c369" @@ -7721,14 +7409,14 @@ minimist@1.1.x: version "1.1.3" resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.1.3.tgz#3bedfd91a92d39016fcfaa1c681e8faa1a1efda8" -minimist@1.2.0, minimist@^1.1.0, minimist@^1.1.1, minimist@^1.1.3, minimist@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284" - minimist@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.1.0.tgz#99df657a52574c21c9057497df742790b2b4c0de" +minimist@^1.1.0, minimist@^1.1.1, minimist@^1.1.3, minimist@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284" + minimist@~0.0.1: version "0.0.10" resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.10.tgz#de3f98543dbf96082be48ad1a0c7cda836301dcf" @@ -7867,14 +7555,6 @@ move-concurrently@^1.0.1: rimraf "^2.5.4" run-queue "^1.0.3" -ms@0.7.1: - version "0.7.1" - resolved "https://registry.yarnpkg.com/ms/-/ms-0.7.1.tgz#9cd13c03adbff25b65effde7ce864ee952017098" - -ms@0.7.2: - version "0.7.2" - resolved "https://registry.yarnpkg.com/ms/-/ms-0.7.2.tgz#ae25cf2512b3885a1d95d7f037868d8431124765" - ms@2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" @@ -8489,10 +8169,6 @@ nth-check@~1.0.1: dependencies: boolbase "~1.0.0" -null-check@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/null-check/-/null-check-1.0.0.tgz#977dffd7176012b9ec30d2a39db5cf72a0439edd" - num2fraction@^1.2.2: version "1.2.2" resolved "https://registry.yarnpkg.com/num2fraction/-/num2fraction-1.2.2.tgz#6f682b6a027a4e9ddfa4564cd2589d1d4e669ede" @@ -8509,18 +8185,10 @@ oauth-sign@~0.8.1, oauth-sign@~0.8.2: version "0.8.2" resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.8.2.tgz#46a6ab7f0aead8deae9ec0565780b7d4efeb9d43" -object-assign@4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.0.tgz#7a3b3d0e98063d43f4c03f2e8ae6cd51a86883a0" - object-assign@4.x, object-assign@^4.0.1, object-assign@^4.1.0, object-assign@^4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" -object-component@0.0.3: - version "0.0.3" - resolved "https://registry.yarnpkg.com/object-component/-/object-component-0.0.3.tgz#f0c69aa50efc95b866c186f400a33769cb2f1291" - object-copy@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/object-copy/-/object-copy-0.1.0.tgz#7e7d858b781bd7c991a41ba975ed3812754e998c" @@ -8659,10 +8327,6 @@ optionator@^0.8.1: type-check "~0.3.2" wordwrap "~1.0.0" -options@>=0.0.5: - version "0.0.6" - resolved "https://registry.yarnpkg.com/options/-/options-0.0.6.tgz#ec22d312806bb53e731773e7cdaefcf1c643128f" - ora@^0.2.3: version "0.2.3" resolved "https://registry.yarnpkg.com/ora/-/ora-0.2.3.tgz#37527d220adcd53c39b73571d754156d5db657a4" @@ -8710,7 +8374,7 @@ os-locale@^2.0.0: lcid "^1.0.0" mem "^1.1.0" -os-tmpdir@^1.0.0, os-tmpdir@^1.0.1, os-tmpdir@~1.0.1, os-tmpdir@~1.0.2: +os-tmpdir@^1.0.0, os-tmpdir@^1.0.1, os-tmpdir@~1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274" @@ -8919,24 +8583,6 @@ parse5@^3.0.1, parse5@^3.0.3: dependencies: "@types/node" "*" -parsejson@0.0.3: - version "0.0.3" - resolved "https://registry.yarnpkg.com/parsejson/-/parsejson-0.0.3.tgz#ab7e3759f209ece99437973f7d0f1f64ae0e64ab" - dependencies: - better-assert "~1.0.0" - -parseqs@0.0.5: - version "0.0.5" - resolved "https://registry.yarnpkg.com/parseqs/-/parseqs-0.0.5.tgz#d5208a3738e46766e291ba2ea173684921a8b89d" - dependencies: - better-assert "~1.0.0" - -parseuri@0.0.5: - version "0.0.5" - resolved "https://registry.yarnpkg.com/parseuri/-/parseuri-0.0.5.tgz#80204a50d4dbb779bfdc6ebe2778d90e4bce320a" - dependencies: - better-assert "~1.0.0" - parseurl@~1.3.2: version "1.3.2" resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.2.tgz#fc289d4ed8993119460c156253262cdc8de65bf3" @@ -9032,7 +8678,7 @@ performance-now@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b" -phantomjs-prebuilt@^2.1.15, phantomjs-prebuilt@^2.1.7: +phantomjs-prebuilt@^2.1.15: version "2.1.16" resolved "https://registry.yarnpkg.com/phantomjs-prebuilt/-/phantomjs-prebuilt-2.1.16.tgz#efd212a4a3966d3647684ea8ba788549be2aefef" dependencies: @@ -9697,10 +9343,6 @@ q@^1.1.2: version "1.5.1" resolved "https://registry.yarnpkg.com/q/-/q-1.5.1.tgz#7e32f75b41381291d04611f1bf14109ac00651d7" -qjobs@^1.1.4: - version "1.2.0" - resolved "https://registry.yarnpkg.com/qjobs/-/qjobs-1.2.0.tgz#c45e9c61800bd087ef88d7e256423bdd49e5d071" - qrcode-terminal@^0.12.0: version "0.12.0" resolved "https://registry.yarnpkg.com/qrcode-terminal/-/qrcode-terminal-0.12.0.tgz#bb5b699ef7f9f0505092a3748be4464fe71b5819" @@ -9709,14 +9351,14 @@ qs@6.5.1: version "6.5.1" resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.1.tgz#349cdf6eef89ec45c12d7d5eb3fc0c870343a6d8" -qs@6.5.2, qs@~6.5.1: - version "6.5.2" - resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36" - qs@~6.3.0: version "6.3.2" resolved "https://registry.yarnpkg.com/qs/-/qs-6.3.2.tgz#e75bd5f6e268122a2a0e0bda630b2550c166502c" +qs@~6.5.1: + version "6.5.2" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36" + query-string@^4.1.0: version "4.3.4" resolved "https://registry.yarnpkg.com/query-string/-/query-string-4.3.4.tgz#bbb693b9ca915c232515b228b1a02b609043dbeb" @@ -9793,7 +9435,7 @@ randomfill@^1.0.3: randombytes "^2.0.5" safe-buffer "^5.1.0" -range-parser@^1.0.3, range-parser@^1.2.0, range-parser@~1.2.0: +range-parser@^1.0.3, range-parser@~1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.0.tgz#f49be6b487894ddc40dcc94a322f611092e00d5e" @@ -9806,15 +9448,6 @@ raw-body@2.3.2: iconv-lite "0.4.19" unpipe "1.0.0" -raw-body@2.3.3: - version "2.3.3" - resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.3.3.tgz#1b324ece6b5706e153855bc1148c65bb7f6ea0c3" - dependencies: - bytes "3.0.0" - http-errors "1.6.3" - iconv-lite "0.4.23" - unpipe "1.0.0" - rc-align@^2.4.0: version "2.4.3" resolved "https://registry.yarnpkg.com/rc-align/-/rc-align-2.4.3.tgz#b9b3c2a6d68adae71a8e1d041cd5e3b2a655f99a" @@ -10101,7 +9734,7 @@ read@1, read@~1.0.1, read@~1.0.7: string_decoder "~1.1.1" util-deprecate "~1.0.1" -readable-stream@1.0, readable-stream@~1.0.2: +readable-stream@1.0: version "1.0.34" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.0.34.tgz#125820e34bc842d2f2aaafafe4c2916ee32c157c" dependencies: @@ -10311,10 +9944,6 @@ repeat-element@^1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/repeat-element/-/repeat-element-1.1.2.tgz#ef089a178d1483baae4d93eb98b4f9e4e11d990a" -repeat-string@^0.2.2: - version "0.2.2" - resolved "https://registry.yarnpkg.com/repeat-string/-/repeat-string-0.2.2.tgz#c7a8d3236068362059a7e4651fc6884e8b1fb4ae" - repeat-string@^1.5.2, repeat-string@^1.6.1: version "1.6.1" resolved "https://registry.yarnpkg.com/repeat-string/-/repeat-string-1.6.1.tgz#8dcae470e1c88abc2d600fff4a776286da75e637" @@ -10526,7 +10155,7 @@ right-align@^0.1.1: dependencies: align-text "^0.1.1" -rimraf@2, rimraf@^2.2.8, rimraf@^2.4.4, rimraf@^2.5.1, rimraf@^2.5.2, rimraf@^2.5.4, rimraf@^2.6.0, rimraf@^2.6.1, rimraf@^2.6.2, rimraf@~2.6.2: +rimraf@2, rimraf@^2.2.8, rimraf@^2.4.4, rimraf@^2.5.1, rimraf@^2.5.2, rimraf@^2.5.4, rimraf@^2.6.1, rimraf@^2.6.2, rimraf@~2.6.2: version "2.6.2" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.2.tgz#2ed8150d24a16ea8651e6d6ef0f47c4158ce7a36" dependencies: @@ -10727,10 +10356,6 @@ semver-diff@^2.0.0: version "5.5.0" resolved "https://registry.yarnpkg.com/semver/-/semver-5.5.0.tgz#dc4bbc7a6ca9d916dee5d43516f0092b58f7b8ab" -semver@~4.3.3: - version "4.3.6" - resolved "https://registry.yarnpkg.com/semver/-/semver-4.3.6.tgz#300bc6e0e86374f7ba61068b5b1ecd57fc6532da" - semver@~5.3.0: version "5.3.0" resolved "https://registry.yarnpkg.com/semver/-/semver-5.3.0.tgz#9b2ce5d3de02d17c6012ad326aa6b4d0cf54f94f" @@ -11059,50 +10684,6 @@ sntp@1.x.x: dependencies: hoek "2.x.x" -socket.io-adapter@0.5.0: - version "0.5.0" - resolved "https://registry.yarnpkg.com/socket.io-adapter/-/socket.io-adapter-0.5.0.tgz#cb6d4bb8bec81e1078b99677f9ced0046066bb8b" - dependencies: - debug "2.3.3" - socket.io-parser "2.3.1" - -socket.io-client@1.7.3: - version "1.7.3" - resolved "https://registry.yarnpkg.com/socket.io-client/-/socket.io-client-1.7.3.tgz#b30e86aa10d5ef3546601c09cde4765e381da377" - dependencies: - backo2 "1.0.2" - component-bind "1.0.0" - component-emitter "1.2.1" - debug "2.3.3" - engine.io-client "1.8.3" - has-binary "0.1.7" - indexof "0.0.1" - object-component "0.0.3" - parseuri "0.0.5" - socket.io-parser "2.3.1" - to-array "0.1.4" - -socket.io-parser@2.3.1: - version "2.3.1" - resolved "https://registry.yarnpkg.com/socket.io-parser/-/socket.io-parser-2.3.1.tgz#dd532025103ce429697326befd64005fcfe5b4a0" - dependencies: - component-emitter "1.1.2" - debug "2.2.0" - isarray "0.0.1" - json3 "3.3.2" - -socket.io@1.7.3: - version "1.7.3" - resolved "https://registry.yarnpkg.com/socket.io/-/socket.io-1.7.3.tgz#b8af9caba00949e568e369f1327ea9be9ea2461b" - dependencies: - debug "2.3.3" - engine.io "1.8.3" - has-binary "0.1.7" - object-assign "4.1.0" - socket.io-adapter "0.5.0" - socket.io-client "1.7.3" - socket.io-parser "2.3.1" - sockjs-client@1.1.4: version "1.1.4" resolved "https://registry.yarnpkg.com/sockjs-client/-/sockjs-client-1.1.4.tgz#5babe386b775e4cf14e7520911452654016c8b12" @@ -11332,10 +10913,6 @@ static-extend@^0.1.1: version "1.5.0" resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c" -statuses@~1.3.1: - version "1.3.1" - resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.3.1.tgz#faf51b9eb74aaef3b3acf4ad5f61abf24cb7b93e" - statuses@~1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.4.0.tgz#bb73d446da2796106efcc1b601a253d6c46bd087" @@ -11749,13 +11326,7 @@ title-case@^2.1.0: no-case "^2.2.0" upper-case "^1.0.3" -tmp@0.0.31: - version "0.0.31" - resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.31.tgz#8f38ab9438e17315e5dbd8b3657e8bfb277ae4a7" - dependencies: - os-tmpdir "~1.0.1" - -tmp@0.0.x, tmp@^0.0.33: +tmp@^0.0.33: version "0.0.33" resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9" dependencies: @@ -11765,10 +11336,6 @@ tmpl@1.0.x: version "1.0.4" resolved "https://registry.yarnpkg.com/tmpl/-/tmpl-1.0.4.tgz#23640dd7b42d00433911140820e5cf440e521dd1" -to-array@0.1.4: - version "0.1.4" - resolved "https://registry.yarnpkg.com/to-array/-/to-array-0.1.4.tgz#17e6c11f73dd4f3d74cda7a4ff3238e9ad9bf890" - to-arraybuffer@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/to-arraybuffer/-/to-arraybuffer-1.0.1.tgz#7d229b1fcc637e466ca081180836a7aabff83f43" @@ -12036,10 +11603,6 @@ uid-number@0.0.6: version "0.0.6" resolved "https://registry.yarnpkg.com/uid-number/-/uid-number-0.0.6.tgz#0ea10e8035e8eb5b8e4449f06da1c730663baa81" -ultron@1.0.x: - version "1.0.2" - resolved "https://registry.yarnpkg.com/ultron/-/ultron-1.0.2.tgz#ace116ab557cd197386a4e88f4685378c8b2e4fa" - umask@^1.1.0, umask@~1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/umask/-/umask-1.1.0.tgz#f29cebf01df517912bb58ff9c4e50fde8e33320d" @@ -12175,10 +11738,6 @@ urix@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/urix/-/urix-0.1.0.tgz#da937f7a62e21fec1fd18d49b35c2935067a6c72" -url-join@^2.0.2: - version "2.0.5" - resolved "https://registry.yarnpkg.com/url-join/-/url-join-2.0.5.tgz#5af22f18c052a000a48d7b82c5e9c2e2feeda728" - url-join@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/url-join/-/url-join-4.0.0.tgz#4d3340e807d3773bda9991f8305acdcc2a665d2a" @@ -12225,13 +11784,6 @@ user-home@^2.0.0: dependencies: os-homedir "^1.0.0" -useragent@^2.1.12: - version "2.3.0" - resolved "https://registry.yarnpkg.com/useragent/-/useragent-2.3.0.tgz#217f943ad540cb2128658ab23fc960f6a88c9972" - dependencies: - lru-cache "4.1.x" - tmp "0.0.x" - util-deprecate@~1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" @@ -12344,10 +11896,6 @@ vm-browserify@0.0.4: dependencies: indexof "0.0.1" -void-elements@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/void-elements/-/void-elements-2.0.1.tgz#c066afb582bb1cb4128d60ea92392e94d5e9dbec" - vue-parser@^1.1.5: version "1.1.6" resolved "https://registry.yarnpkg.com/vue-parser/-/vue-parser-1.1.6.tgz#3063c8431795664ebe429c23b5506899706e6355" @@ -12492,18 +12040,6 @@ webpack-dev-middleware@3.1.3: url-join "^4.0.0" webpack-log "^1.0.1" -webpack-dev-middleware@^2.0.6: - version "2.0.6" - resolved "https://registry.yarnpkg.com/webpack-dev-middleware/-/webpack-dev-middleware-2.0.6.tgz#a51692801e8310844ef3e3790e1eacfe52326fd4" - dependencies: - loud-rejection "^1.6.0" - memory-fs "~0.4.1" - mime "^2.1.0" - path-is-absolute "^1.0.0" - range-parser "^1.0.3" - url-join "^2.0.2" - webpack-log "^1.0.1" - webpack-dev-server@^3.1.0: version "3.1.4" resolved "https://registry.yarnpkg.com/webpack-dev-server/-/webpack-dev-server-3.1.4.tgz#9a08d13c4addd1e3b6d8ace116e86715094ad5b4" @@ -12638,7 +12174,7 @@ which-pm-runs@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/which-pm-runs/-/which-pm-runs-1.0.0.tgz#670b3afbc552e0b55df6b7780ca74615f23ad1cb" -which@1, which@^1.2.1, which@^1.2.10, which@^1.2.12, which@^1.2.14, which@^1.2.4, which@^1.2.9, which@^1.3.0, which@~1.3.0: +which@1, which@^1.2.10, which@^1.2.12, which@^1.2.14, which@^1.2.4, which@^1.2.9, which@^1.3.0, which@~1.3.0: version "1.3.1" resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a" dependencies: @@ -12717,13 +12253,6 @@ write@^0.2.1: dependencies: mkdirp "^0.5.1" -ws@1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/ws/-/ws-1.1.2.tgz#8a244fa052401e08c9886cf44a85189e1fd4067f" - dependencies: - options ">=0.0.5" - ultron "1.0.x" - ws@^4.0.0: version "4.1.0" resolved "https://registry.yarnpkg.com/ws/-/ws-4.1.0.tgz#a979b5d7d4da68bf54efe0408967c324869a7289" @@ -12731,10 +12260,6 @@ ws@^4.0.0: async-limiter "~1.0.0" safe-buffer "~5.1.0" -wtf-8@1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/wtf-8/-/wtf-8-1.0.0.tgz#392d8ba2d0f1c34d1ee2d630f15d0efb68e1048a" - xdg-basedir@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/xdg-basedir/-/xdg-basedir-3.0.0.tgz#496b2cc109eca8dbacfe2dc72b603c17c5870ad4" @@ -12747,10 +12272,6 @@ xml-name-validator@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/xml-name-validator/-/xml-name-validator-3.0.0.tgz#6ae73e06de4d8c6e47f9fb181f78d648ad457c6a" -xmlhttprequest-ssl@1.5.3: - version "1.5.3" - resolved "https://registry.yarnpkg.com/xmlhttprequest-ssl/-/xmlhttprequest-ssl-1.5.3.tgz#185a888c04eca46c3e4070d99f7b49de3528992d" - xmlhttprequest@1: version "1.8.0" resolved "https://registry.yarnpkg.com/xmlhttprequest/-/xmlhttprequest-1.8.0.tgz#67fe075c5c24fef39f9d65f5f7b7fe75171968fc" @@ -12883,10 +12404,6 @@ yauzl@2.4.1: dependencies: fd-slicer "~1.0.1" -yeast@0.1.2: - version "0.1.2" - resolved "https://registry.yarnpkg.com/yeast/-/yeast-0.1.2.tgz#008e06d8094320c372dbc2f8ed76a0ca6c8ac419" - yeoman-environment@^2.0.5, yeoman-environment@^2.1.1: version "2.2.0" resolved "https://registry.yarnpkg.com/yeoman-environment/-/yeoman-environment-2.2.0.tgz#6c0ee93a8d962a9f6dbc5ad4e90ae7ab34875393" From 6225efa50ccdcd1a80fe4deeac2570deffcbac06 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Tue, 14 Aug 2018 11:24:08 +0200 Subject: [PATCH 421/786] docs: update postgres provisioning --- docs/sources/features/datasources/postgres.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/sources/features/datasources/postgres.md b/docs/sources/features/datasources/postgres.md index e2dcf888025..4afde5cc6cb 100644 --- a/docs/sources/features/datasources/postgres.md +++ b/docs/sources/features/datasources/postgres.md @@ -290,4 +290,5 @@ datasources: password: "Password!" jsonData: sslmode: "disable" # disable/require/verify-ca/verify-full + timescaledb: false ``` From 3769df7119ca3c26220b37f68804ca03a8b32e52 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Tue, 14 Aug 2018 12:16:46 +0200 Subject: [PATCH 422/786] changelog: add notes about closing #12680 [skip ci] --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0c397e45ea4..4890a471ac9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ * **Configuration**: Allow auto-assigning users to specific organization (other than Main. Org) [#1823](https://github.com/grafana/grafana/issues/1823) [#12801](https://github.com/grafana/grafana/issues/12801), thx [@gzzo](https://github.com/gzzo) and [@ofosos](https://github.com/ofosos) * **Profile**: List teams that the user is member of in current/active organization [#12476](https://github.com/grafana/grafana/issues/12476) * **LDAP**: Client certificates support [#12805](https://github.com/grafana/grafana/issues/12805), thx [@nyxi](https://github.com/nyxi) +* **Postgres**: TimescaleDB support, e.g. use `time_bucket` for grouping by time when option enabled [#12680](https://github.com/grafana/grafana/pull/12680), thx [svenklemm](https://github.com/svenklemm) ### Minor From a1ed3ae0943fb54c7af4ab156beaf1c883300685 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Tue, 14 Aug 2018 12:25:19 +0200 Subject: [PATCH 423/786] feat: add auto fit panels to shortcut modal, closes #12768 --- public/app/core/components/help/help.ts | 1 + public/app/core/services/keybindingSrv.ts | 15 +++------------ 2 files changed, 4 insertions(+), 12 deletions(-) diff --git a/public/app/core/components/help/help.ts b/public/app/core/components/help/help.ts index a1d3c34ae5b..eac47b6e0a2 100644 --- a/public/app/core/components/help/help.ts +++ b/public/app/core/components/help/help.ts @@ -25,6 +25,7 @@ export class HelpCtrl { { keys: ['d', 'k'], description: 'Toggle kiosk mode (hides top nav)' }, { keys: ['d', 'E'], description: 'Expand all rows' }, { keys: ['d', 'C'], description: 'Collapse all rows' }, + { keys: ['d', 'a'], description: 'Toggle auto fit panels (experimental feature)' }, { keys: ['mod+o'], description: 'Toggle shared graph crosshair' }, ], 'Focused Panel': [ diff --git a/public/app/core/services/keybindingSrv.ts b/public/app/core/services/keybindingSrv.ts index f740718063c..9d914a94a1c 100644 --- a/public/app/core/services/keybindingSrv.ts +++ b/public/app/core/services/keybindingSrv.ts @@ -15,14 +15,7 @@ export class KeybindingSrv { timepickerOpen = false; /** @ngInject */ - constructor( - private $rootScope, - private $location, - private datasourceSrv, - private timeSrv, - private contextSrv, - private $route - ) { + constructor(private $rootScope, private $location, private datasourceSrv, private timeSrv, private contextSrv) { // clear out all shortcuts on route change $rootScope.$on('$routeChangeSuccess', () => { Mousetrap.reset(); @@ -269,10 +262,8 @@ export class KeybindingSrv { //Autofit panels this.bind('d a', () => { - this.$location.search('autofitpanels', this.$location.search().autofitpanels ? null : true); - //Force reload - - this.$route.reload(); + // this has to be a full page reload + window.location.href = window.location.href + '&autofitpanels'; }); } } From de25a4fe4ed8459c234916d39ce58cbbe5fb6669 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Tue, 14 Aug 2018 12:40:07 +0200 Subject: [PATCH 424/786] docs: update --- .github/CONTRIBUTING.md | 8 ++------ README.md | 11 +++++------ docs/sources/project/building_from_source.md | 13 ++++++------- 3 files changed, 13 insertions(+), 19 deletions(-) diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 14c6c07ab16..769ba2a519b 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -2,15 +2,11 @@ Follow the setup guide in README.md ### Rebuild frontend assets on source change ``` -grunt && grunt watch +yarn watch ``` ### Rerun tests on source change ``` -npm run jest -``` -or -``` yarn jest ``` @@ -21,6 +17,6 @@ test -z "$(gofmt -s -l . | grep -v -E 'vendor/(github.com|golang.org|gopkg.in)' ### Run tests for frontend assets before commit ``` -npm test +yarn test go test -v ./pkg/... ``` diff --git a/README.md b/README.md index 71fdb04cea6..74fb10c8066 100644 --- a/README.md +++ b/README.md @@ -43,7 +43,7 @@ To build the assets, rebuild on file change, and serve them by Grafana's webserv ```bash npm install -g yarn yarn install --pure-lockfile -yarn run watch +yarn watch ``` Build the assets, rebuild on file change with Hot Module Replacement (HMR), and serve them by webpack-dev-server (http://localhost:3333): @@ -56,7 +56,7 @@ Note: HMR for Angular is not supported. If you edit files in the Angular part of Run tests ```bash -yarn run jest +yarn jest ``` ### Recompile backend on source change @@ -93,14 +93,13 @@ In your custom.ini uncomment (remove the leading `;`) sign. And set `app_mode = #### Frontend Execute all frontend tests ```bash -yarn run test +yarn test ``` Writing & watching frontend tests -- jest for all new tests that do not require browser context (React+more) - - Start watcher: `yarn run jest` - - Jest will run all test files that end with the name ".test.ts" +- Start watcher: `yarn jest` +- Jest will run all test files that end with the name ".test.ts" #### Backend ```bash diff --git a/docs/sources/project/building_from_source.md b/docs/sources/project/building_from_source.md index 20c177211e3..08673404572 100644 --- a/docs/sources/project/building_from_source.md +++ b/docs/sources/project/building_from_source.md @@ -57,7 +57,7 @@ For this you need nodejs (v.6+). ```bash npm install -g yarn yarn install --pure-lockfile -npm run watch +yarn watch ``` ## Running Grafana Locally @@ -83,18 +83,17 @@ go get github.com/Unknwon/bra bra run ``` -You'll also need to run `npm run watch` to watch for changes to the front-end (typescript, html, sass) +You'll also need to run `yarn watch` to watch for changes to the front-end (typescript, html, sass) ### Running tests -- You can run backend Golang tests using "go test ./pkg/...". -- Execute all frontend tests with "npm run test" +- You can run backend Golang tests using `go test ./pkg/...`. +- Execute all frontend tests with `yarn test` Writing & watching frontend tests -- jest for all new tests that do not require browser context (React+more) - - Start watcher: `npm run jest` - - Jest will run all test files that end with the name ".test.ts" +- Start watcher: `yarn jest` +- Jest will run all test files that end with the name ".test.ts" ## Creating optimized release packages From e6ea8f7e0bd3df2677411846261bbbad72154a7f Mon Sep 17 00:00:00 2001 From: Patrick O'Carroll Date: Tue, 14 Aug 2018 13:21:52 +0200 Subject: [PATCH 425/786] added guide for logging in to grafana for the first and how to add a datasource --- docs/sources/guides/getting_started.md | 25 ++++++++++++++++++++ docs/sources/installation/debian.md | 6 +++++ docs/sources/installation/docker.md | 6 +++++ docs/sources/installation/mac.md | 5 ++++ docs/sources/installation/rpm.md | 5 ++++ docs/sources/installation/windows.md | 6 +++++ docs/sources/project/building_from_source.md | 6 +++++ 7 files changed, 59 insertions(+) diff --git a/docs/sources/guides/getting_started.md b/docs/sources/guides/getting_started.md index f724504156f..fcb7ff9b060 100644 --- a/docs/sources/guides/getting_started.md +++ b/docs/sources/guides/getting_started.md @@ -15,6 +15,31 @@ weight = 1 This guide will help you get started and acquainted with Grafana. It assumes you have a working Grafana server up and running and have added at least one [Data Source](/features/datasources/). +## Logging in for the first time + +To run Grafana open your browser and go to port 3000 which is the default port. If you have changed the port you go to that port. There you will see the login page. User name is admin and password is admin. When you log in for the first time you will be asked to change your password. You can later go to user preferences and change your user name. + + +## How to add a data source + +{{< docs-imagebox img="/img/docs/v52/sidemenu-datasource.png" max-width="250px" class="docs-image--right docs-image--no-shadow">}} + +Before you create your first dashboard you need to add your data source. + +First move your cursor to the cog on the side menu which will show you the configuration menu. If the side menu is not visible click the Grafana icon in the upper left corner. The first item on the configuration menu is data sources. Click and you will come to data sources. You can also simply click the cog. + + +Click Add data source and you will come to the settings page of your new data source. + +{{< docs-imagebox img="/img/docs/v52/add-datasource.png" max-width="700px" class="docs-image--no-shadow">}} + +The first thing you will do is give the data source a name and select the right type. +Next you need to specify the data sources HTTP URL and how you will access the data source. + +{{< docs-imagebox img="/img/docs/v52/datasource-settings.png" max-width="700px" class="docs-image--no-shadow">}} + +Now you are ready to save and test. + ## Beginner guides Watch the 10min [beginners guide to building dashboards](https://www.youtube.com/watch?v=sKNZMtoSHN4&index=7&list=PLDGkOdUX1Ujo3wHw9-z5Vo12YLqXRjzg2) to get a quick intro to setting up Dashboards and Panels. diff --git a/docs/sources/installation/debian.md b/docs/sources/installation/debian.md index 4bb245a586e..e9504c7cbf3 100644 --- a/docs/sources/installation/debian.md +++ b/docs/sources/installation/debian.md @@ -166,3 +166,9 @@ To configure Grafana add a configuration file named `custom.ini` to the Start Grafana by executing `./bin/grafana-server web`. The `grafana-server` binary needs the working directory to be the root install directory (where the binary and the `public` folder is located). + +## Logging in for the first time + +To run Grafana open your browser and go to port 3000 which is the default port. If you have changed the port you go to that port. There you will see the login page. User name is admin and password is admin. When you log in for the first time you will be asked to change your password. You can later go to user preferences and change your user name. + +Here you can get help [getting started](https://www.youtube.com/watch?v=sKNZMtoSHN4&index=7&list=PLDGkOdUX1Ujo3wHw9-z5Vo12YLqXRjzg2) with your dashboards. \ No newline at end of file diff --git a/docs/sources/installation/docker.md b/docs/sources/installation/docker.md index 1f755625699..719af9a4e05 100644 --- a/docs/sources/installation/docker.md +++ b/docs/sources/installation/docker.md @@ -212,3 +212,9 @@ chown -R root:root /etc/grafana && \ chown -R grafana:grafana /var/lib/grafana && \ chown -R grafana:grafana /usr/share/grafana ``` + +## Logging in for the first time + +To run Grafana open your browser and go to port 3000 which is the default port. If you have changed the port you go to that port. There you will see the login page. User name is admin and password is admin. When you log in for the first time you will be asked to change your password. You can later go to user preferences and change your user name. + +Here you can get help [getting started](https://www.youtube.com/watch?v=sKNZMtoSHN4&index=7&list=PLDGkOdUX1Ujo3wHw9-z5Vo12YLqXRjzg2) with your dashboards. \ No newline at end of file diff --git a/docs/sources/installation/mac.md b/docs/sources/installation/mac.md index 12ff4adaab9..72ec0871646 100644 --- a/docs/sources/installation/mac.md +++ b/docs/sources/installation/mac.md @@ -92,3 +92,8 @@ Start Grafana by executing `./bin/grafana-server web`. The `grafana-server` binary needs the working directory to be the root install directory (where the binary and the `public` folder is located). +## Logging in for the first time + +To run Grafana open your browser and go to port 3000 which is the default port. If you have changed the port you go to that port. There you will see the login page. User name is admin and password is admin. When you log in for the first time you will be asked to change your password. You can later go to user preferences and change your user name. + +Here you can get help [getting started](https://www.youtube.com/watch?v=sKNZMtoSHN4&index=7&list=PLDGkOdUX1Ujo3wHw9-z5Vo12YLqXRjzg2) with your dashboards. \ No newline at end of file diff --git a/docs/sources/installation/rpm.md b/docs/sources/installation/rpm.md index 13597b9d921..0f50ed026b8 100644 --- a/docs/sources/installation/rpm.md +++ b/docs/sources/installation/rpm.md @@ -193,3 +193,8 @@ Start Grafana by executing `./bin/grafana-server web`. The `grafana-server` binary needs the working directory to be the root install directory (where the binary and the `public` folder is located). +## Logging in for the first time + +To run Grafana open your browser and go to port 3000 which is the default port. If you have changed the port you go to that port. There you will see the login page. User name is admin and password is admin. When you log in for the first time you will be asked to change your password. You can later go to user preferences and change your user name. + +Here you can get help [getting started](https://www.youtube.com/watch?v=sKNZMtoSHN4&index=7&list=PLDGkOdUX1Ujo3wHw9-z5Vo12YLqXRjzg2) with your dashboards. \ No newline at end of file diff --git a/docs/sources/installation/windows.md b/docs/sources/installation/windows.md index 5dc87984512..5bd66b8ac6d 100644 --- a/docs/sources/installation/windows.md +++ b/docs/sources/installation/windows.md @@ -43,3 +43,9 @@ Read more about the [configuration options]({{< relref "configuration.md" >}}). The Grafana backend includes Sqlite3 which requires GCC to compile. So in order to compile Grafana on Windows you need to install GCC. We recommend [TDM-GCC](http://tdm-gcc.tdragon.net/download). + +## Logging in for the first time + +To run Grafana open your browser and go to port 3000 which is the default port. If you have changed the port you go to that port. There you will see the login page. User name is admin and password is admin. When you log in for the first time you will be asked to change your password. You can later go to user preferences and change your user name. + +Here you can get help [getting started](https://www.youtube.com/watch?v=sKNZMtoSHN4&index=7&list=PLDGkOdUX1Ujo3wHw9-z5Vo12YLqXRjzg2) with your dashboards. \ No newline at end of file diff --git a/docs/sources/project/building_from_source.md b/docs/sources/project/building_from_source.md index a0b553594ce..6a9e56a5eda 100644 --- a/docs/sources/project/building_from_source.md +++ b/docs/sources/project/building_from_source.md @@ -144,3 +144,9 @@ Please contribute to the Grafana project and submit a pull request! Build new fe **Problem**: On Windows, getting errors about a tool not being installed even though you just installed that tool. **Solution**: It is usually because it got added to the path and you have to restart your command prompt to use it. + +## Logging in for the first time + +To run Grafana open your browser and go to port 3000 which is the default port. If you have changed the port you go to that port. There you will see the login page. User name is admin and password is admin. When you log in for the first time you will be asked to change your password. You can later go to user preferences and change your user name. + +Here you can get help [getting started](https://www.youtube.com/watch?v=sKNZMtoSHN4&index=7&list=PLDGkOdUX1Ujo3wHw9-z5Vo12YLqXRjzg2) with your dashboards. \ No newline at end of file From 332e59d31400f9ce250c45a121de63fc8a53ed62 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Tue, 14 Aug 2018 13:42:18 +0200 Subject: [PATCH 426/786] changelog: add notes about closing #12224 [skip ci] --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4890a471ac9..4bd9cb917d7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -61,6 +61,10 @@ These are new features that's still being worked on and are in an experimental p * **Dashboard**: Auto fit dashboard panels to optimize space used for current TV / Monitor [#12768](https://github.com/grafana/grafana/issues/12768) +### Tech + +* **Frontend**: Convert all Frontend Karma tests to Jest tests [#12224](https://github.com/grafana/grafana/issues/12224) + # 5.2.2 (2018-07-25) ### Minor From aefcb06ff823c8248f0f1ec03ce2d9578f1ea01d Mon Sep 17 00:00:00 2001 From: Leonard Gram Date: Tue, 14 Aug 2018 10:45:32 +0200 Subject: [PATCH 427/786] build: verifies the rpm packages signatures. Closes #12370 --- .circleci/config.yml | 5 +++++ scripts/build/verify_signed_packages.sh | 17 +++++++++++++++++ 2 files changed, 22 insertions(+) create mode 100755 scripts/build/verify_signed_packages.sh diff --git a/.circleci/config.yml b/.circleci/config.yml index 977121c30ee..c2e4cce9c4b 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -147,6 +147,11 @@ jobs: - run: name: sign packages command: './scripts/build/sign_packages.sh' + - run: + name: verify signed packages + command: | + curl https://grafanarel.s3.amazonaws.com/RPM-GPG-KEY-grafana > ~/.rpmdb/pubkeys/grafana.key + ./scripts/build/verify_signed_packages.sh dist/*.rpm - run: name: sha-sum packages command: 'go run build.go sha-dist' diff --git a/scripts/build/verify_signed_packages.sh b/scripts/build/verify_signed_packages.sh new file mode 100755 index 00000000000..c3e5b09afc2 --- /dev/null +++ b/scripts/build/verify_signed_packages.sh @@ -0,0 +1,17 @@ +#!/bin/bash +_files=$* + +ALL_SIGNED=0 + +for file in $_files; do + rpm -K "$file" | grep "pgp.*OK" -q + if [[ $? != 0 ]]; then + ALL_SIGNED=1 + echo $file NOT SIGNED + else + echo $file OK + fi +done + + +exit $ALL_SIGNED From 7ec146df9989e407b816b51069c8cf9bd4eb43cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Knecht?= Date: Thu, 15 Feb 2018 19:13:47 +0100 Subject: [PATCH 428/786] social: add GitLab authentication backend GitLab could already be used as an authentication backend by properly configuring `auth.generic_oauth`, but then there was no way to authorize users based on their GitLab group membership. This commit adds a `auth.gitlab` backend, similar to `auth.github`, with an `allowed_groups` option that can be set to a list of groups whose members should be allowed access to Grafana. --- conf/defaults.ini | 12 +++ pkg/models/models.go | 1 + pkg/social/gitlab_oauth.go | 131 +++++++++++++++++++++++++++++++++ pkg/social/social.go | 16 +++- public/app/partials/login.html | 4 + public/sass/_variables.scss | 1 + 6 files changed, 164 insertions(+), 1 deletion(-) create mode 100644 pkg/social/gitlab_oauth.go diff --git a/conf/defaults.ini b/conf/defaults.ini index 99c1537eb95..90fc144c6e0 100644 --- a/conf/defaults.ini +++ b/conf/defaults.ini @@ -270,6 +270,18 @@ api_url = https://api.github.com/user team_ids = allowed_organizations = +#################################### GitLab Auth ######################### +[auth.gitlab] +enabled = false +allow_sign_up = true +client_id = some_id +client_secret = some_secret +scopes = api +auth_url = https://gitlab.com/oauth/authorize +token_url = https://gitlab.com/oauth/token +api_url = https://gitlab.com/api/v4 +allowed_groups = + #################################### Google Auth ######################### [auth.google] enabled = false diff --git a/pkg/models/models.go b/pkg/models/models.go index c2560021ee1..ba894ae591f 100644 --- a/pkg/models/models.go +++ b/pkg/models/models.go @@ -8,4 +8,5 @@ const ( TWITTER GENERIC GRAFANA_COM + GITLAB ) diff --git a/pkg/social/gitlab_oauth.go b/pkg/social/gitlab_oauth.go new file mode 100644 index 00000000000..22e50b9653a --- /dev/null +++ b/pkg/social/gitlab_oauth.go @@ -0,0 +1,131 @@ +package social + +import ( + "encoding/json" + "fmt" + "net/http" + "regexp" + + "github.com/grafana/grafana/pkg/models" + + "golang.org/x/oauth2" +) + +type SocialGitlab struct { + *SocialBase + allowedDomains []string + allowedGroups []string + apiUrl string + allowSignup bool +} + +var ( + ErrMissingGroupMembership = &Error{"User not a member of one of the required groups"} +) + +func (s *SocialGitlab) Type() int { + return int(models.GITLAB) +} + +func (s *SocialGitlab) IsEmailAllowed(email string) bool { + return isEmailAllowed(email, s.allowedDomains) +} + +func (s *SocialGitlab) IsSignupAllowed() bool { + return s.allowSignup +} + +func (s *SocialGitlab) IsGroupMember(client *http.Client) bool { + if len(s.allowedGroups) == 0 { + return true + } + + for groups, url := s.GetGroups(client, s.apiUrl+"/groups"); groups != nil; groups, url = s.GetGroups(client, url) { + for _, allowedGroup := range s.allowedGroups { + for _, group := range groups { + if group == allowedGroup { + return true + } + } + } + } + + return false +} + +func (s *SocialGitlab) GetGroups(client *http.Client, url string) ([]string, string) { + type Group struct { + FullPath string `json:"full_path"` + } + + var ( + groups []Group + next string + ) + + if url == "" { + return nil, next + } + + response, err := HttpGet(client, url) + if err != nil { + s.log.Error("Error getting groups from GitLab API", "err", err) + return nil, next + } + + if err := json.Unmarshal(response.Body, &groups); err != nil { + s.log.Error("Error parsing JSON from GitLab API", "err", err) + return nil, next + } + + fullPaths := make([]string, len(groups)) + for i, group := range groups { + fullPaths[i] = group.FullPath + } + + if link, ok := response.Headers["Link"]; ok { + pattern := regexp.MustCompile(`<([^>]+)>; rel="next"`) + if matches := pattern.FindStringSubmatch(link[0]); matches != nil { + next = matches[1] + } + } + + return fullPaths, next +} + +func (s *SocialGitlab) UserInfo(client *http.Client, token *oauth2.Token) (*BasicUserInfo, error) { + + var data struct { + Id int + Username string + Email string + Name string + State string + } + + response, err := HttpGet(client, s.apiUrl+"/user") + if err != nil { + return nil, fmt.Errorf("Error getting user info: %s", err) + } + + err = json.Unmarshal(response.Body, &data) + if err != nil { + return nil, fmt.Errorf("Error getting user info: %s", err) + } + + if data.State != "active" { + return nil, fmt.Errorf("User %s is inactive", data.Username) + } + + userInfo := &BasicUserInfo{ + Name: data.Name, + Login: data.Username, + Email: data.Email, + } + + if !s.IsGroupMember(client) { + return nil, ErrMissingGroupMembership + } + + return userInfo, nil +} diff --git a/pkg/social/social.go b/pkg/social/social.go index adbe5a912d9..2be71514629 100644 --- a/pkg/social/social.go +++ b/pkg/social/social.go @@ -55,7 +55,7 @@ func NewOAuthService() { setting.OAuthService = &setting.OAuther{} setting.OAuthService.OAuthInfos = make(map[string]*setting.OAuthInfo) - allOauthes := []string{"github", "google", "generic_oauth", "grafananet", "grafana_com"} + allOauthes := []string{"github", "gitlab", "google", "generic_oauth", "grafananet", "grafana_com"} for _, name := range allOauthes { sec := setting.Raw.Section("auth." + name) @@ -115,6 +115,20 @@ func NewOAuthService() { } } + // GitLab. + if name == "gitlab" { + SocialMap["gitlab"] = &SocialGitlab{ + SocialBase: &SocialBase{ + Config: &config, + log: logger, + }, + allowedDomains: info.AllowedDomains, + apiUrl: info.ApiUrl, + allowSignup: info.AllowSignup, + allowedGroups: util.SplitString(sec.Key("allowed_groups").String()), + } + } + // Google. if name == "google" { SocialMap["google"] = &SocialGoogle{ diff --git a/public/app/partials/login.html b/public/app/partials/login.html index 1919759334b..87b3cada7b5 100644 --- a/public/app/partials/login.html +++ b/public/app/partials/login.html @@ -51,6 +51,10 @@ Sign in with GitHub +
    {group.groupId}
    diff --git a/public/app/containers/Teams/TeamPages.tsx b/public/app/containers/Teams/TeamPages.tsx index 500a7cbe5e8..2abc9c51535 100644 --- a/public/app/containers/Teams/TeamPages.tsx +++ b/public/app/containers/Teams/TeamPages.tsx @@ -5,7 +5,7 @@ import { inject, observer } from 'mobx-react'; import config from 'app/core/config'; import PageHeader from 'app/core/components/PageHeader/PageHeader'; import { NavStore } from 'app/stores/NavStore/NavStore'; -import { TeamsStore, ITeam } from 'app/stores/TeamsStore/TeamsStore'; +import { TeamsStore, Team } from 'app/stores/TeamsStore/TeamsStore'; import { ViewStore } from 'app/stores/ViewStore/ViewStore'; import TeamMembers from './TeamMembers'; import TeamSettings from './TeamSettings'; @@ -40,7 +40,7 @@ export class TeamPages extends React.Component { nav.initTeamPage(this.getCurrentTeam(), this.currentPage, this.isSyncEnabled); } - getCurrentTeam(): ITeam { + getCurrentTeam(): Team { const { teams, view } = this.props; return teams.map.get(view.routeParams.get('id')); } diff --git a/public/app/containers/Teams/TeamSettings.tsx b/public/app/containers/Teams/TeamSettings.tsx index 142088a5d1e..0de60a0b16c 100644 --- a/public/app/containers/Teams/TeamSettings.tsx +++ b/public/app/containers/Teams/TeamSettings.tsx @@ -1,11 +1,11 @@ import React from 'react'; import { hot } from 'react-hot-loader'; import { observer } from 'mobx-react'; -import { ITeam } from 'app/stores/TeamsStore/TeamsStore'; +import { Team } from 'app/stores/TeamsStore/TeamsStore'; import { Label } from 'app/core/components/Forms/Forms'; interface Props { - team: ITeam; + team: Team; } @observer diff --git a/public/app/core/components/EmptyListCTA/EmptyListCTA.tsx b/public/app/core/components/EmptyListCTA/EmptyListCTA.tsx index 1583303dfa1..5ece360e36a 100644 --- a/public/app/core/components/EmptyListCTA/EmptyListCTA.tsx +++ b/public/app/core/components/EmptyListCTA/EmptyListCTA.tsx @@ -1,34 +1,37 @@ import React, { Component } from 'react'; -export interface IProps { - model: any; +export interface Props { + model: any; } -class EmptyListCTA extends Component { - render() { - const { - title, - buttonIcon, - buttonLink, - buttonTitle, - proTip, - proTipLink, - proTipLinkTitle, - proTipTarget - } = this.props.model; - return ( -
    -
    {title}
    - {buttonTitle} -
    - ProTip: {proTip} - {proTipLinkTitle} -
    -
    - ); - } +class EmptyListCTA extends Component { + render() { + const { + title, + buttonIcon, + buttonLink, + buttonTitle, + proTip, + proTipLink, + proTipLinkTitle, + proTipTarget, + } = this.props.model; + return ( +
    +
    {title}
    + + + {buttonTitle} + +
    + ProTip: {proTip} + + {proTipLinkTitle} + +
    +
    + ); + } } export default EmptyListCTA; diff --git a/public/app/core/components/PageHeader/PageHeader.tsx b/public/app/core/components/PageHeader/PageHeader.tsx index f998cb9981f..1d744b7e609 100644 --- a/public/app/core/components/PageHeader/PageHeader.tsx +++ b/public/app/core/components/PageHeader/PageHeader.tsx @@ -5,7 +5,7 @@ import classNames from 'classnames'; import appEvents from 'app/core/app_events'; import { toJS } from 'mobx'; -export interface IProps { +export interface Props { model: NavModel; } @@ -82,7 +82,7 @@ const Navigation = ({ main }: { main: NavModelItem }) => { }; @observer -export default class PageHeader extends React.Component { +export default class PageHeader extends React.Component { constructor(props) { super(props); } diff --git a/public/app/core/components/PasswordStrength.tsx b/public/app/core/components/PasswordStrength.tsx index 8f92b18445c..1d676a00a37 100644 --- a/public/app/core/components/PasswordStrength.tsx +++ b/public/app/core/components/PasswordStrength.tsx @@ -1,32 +1,31 @@ import React from 'react'; -export interface IProps { +export interface Props { password: string; } -export class PasswordStrength extends React.Component { - +export class PasswordStrength extends React.Component { constructor(props) { super(props); } render() { const { password } = this.props; - let strengthText = "strength: strong like a bull."; - let strengthClass = "password-strength-good"; + let strengthText = 'strength: strong like a bull.'; + let strengthClass = 'password-strength-good'; if (!password) { return null; } if (password.length <= 8) { - strengthText = "strength: you can do better."; - strengthClass = "password-strength-ok"; + strengthText = 'strength: you can do better.'; + strengthClass = 'password-strength-ok'; } if (password.length < 4) { - strengthText = "strength: weak sauce."; - strengthClass = "password-strength-bad"; + strengthText = 'strength: weak sauce.'; + strengthClass = 'password-strength-bad'; } return ( @@ -36,5 +35,3 @@ export class PasswordStrength extends React.Component { ); } } - - diff --git a/public/app/core/components/Permissions/DisabledPermissionsListItem.tsx b/public/app/core/components/Permissions/DisabledPermissionsListItem.tsx index bbb9754fe0d..d65595dae66 100644 --- a/public/app/core/components/Permissions/DisabledPermissionsListItem.tsx +++ b/public/app/core/components/Permissions/DisabledPermissionsListItem.tsx @@ -2,11 +2,11 @@ import React, { Component } from 'react'; import DescriptionPicker from 'app/core/components/Picker/DescriptionPicker'; import { permissionOptions } from 'app/stores/PermissionsStore/PermissionsStore'; -export interface IProps { +export interface Props { item: any; } -export default class DisabledPermissionListItem extends Component { +export default class DisabledPermissionListItem extends Component { render() { const { item } = this.props; diff --git a/public/app/core/components/Permissions/Permissions.tsx b/public/app/core/components/Permissions/Permissions.tsx index dbdc1682f6b..d17899c891f 100644 --- a/public/app/core/components/Permissions/Permissions.tsx +++ b/public/app/core/components/Permissions/Permissions.tsx @@ -20,7 +20,7 @@ export interface DashboardAcl { sortRank?: number; } -export interface IProps { +export interface Props { dashboardId: number; folderInfo?: FolderInfo; permissions?: any; @@ -29,7 +29,7 @@ export interface IProps { } @observer -class Permissions extends Component { +class Permissions extends Component { constructor(props) { super(props); const { dashboardId, isFolder, folderInfo } = this.props; diff --git a/public/app/core/components/Permissions/PermissionsList.tsx b/public/app/core/components/Permissions/PermissionsList.tsx index a77235ecc30..7e64de012e4 100644 --- a/public/app/core/components/Permissions/PermissionsList.tsx +++ b/public/app/core/components/Permissions/PermissionsList.tsx @@ -4,7 +4,7 @@ import DisabledPermissionsListItem from './DisabledPermissionsListItem'; import { observer } from 'mobx-react'; import { FolderInfo } from './FolderInfo'; -export interface IProps { +export interface Props { permissions: any[]; removeItem: any; permissionChanged: any; @@ -13,7 +13,7 @@ export interface IProps { } @observer -class PermissionsList extends Component { +class PermissionsList extends Component { render() { const { permissions, removeItem, permissionChanged, fetching, folderInfo } = this.props; diff --git a/public/app/core/components/Picker/DescriptionOption.tsx b/public/app/core/components/Picker/DescriptionOption.tsx index 12a1fdd9163..1bcb7100489 100644 --- a/public/app/core/components/Picker/DescriptionOption.tsx +++ b/public/app/core/components/Picker/DescriptionOption.tsx @@ -1,6 +1,6 @@ import React, { Component } from 'react'; -export interface IProps { +export interface Props { onSelect: any; onFocus: any; option: any; @@ -8,7 +8,7 @@ export interface IProps { className: any; } -class DescriptionOption extends Component { +class DescriptionOption extends Component { constructor(props) { super(props); this.handleMouseDown = this.handleMouseDown.bind(this); diff --git a/public/app/core/components/Picker/PickerOption.tsx b/public/app/core/components/Picker/PickerOption.tsx index 1b32adac572..f30a7c06d10 100644 --- a/public/app/core/components/Picker/PickerOption.tsx +++ b/public/app/core/components/Picker/PickerOption.tsx @@ -1,6 +1,6 @@ import React, { Component } from 'react'; -export interface IProps { +export interface Props { onSelect: any; onFocus: any; option: any; @@ -8,7 +8,7 @@ export interface IProps { className: any; } -class UserPickerOption extends Component { +class UserPickerOption extends Component { constructor(props) { super(props); this.handleMouseDown = this.handleMouseDown.bind(this); diff --git a/public/app/core/components/TagFilter/TagBadge.tsx b/public/app/core/components/TagFilter/TagBadge.tsx index e5c2e357a58..d93b5fd1e74 100644 --- a/public/app/core/components/TagFilter/TagBadge.tsx +++ b/public/app/core/components/TagFilter/TagBadge.tsx @@ -1,14 +1,14 @@ import React from 'react'; import tags from 'app/core/utils/tags'; -export interface IProps { +export interface Props { label: string; removeIcon: boolean; count: number; onClick: any; } -export class TagBadge extends React.Component { +export class TagBadge extends React.Component { constructor(props) { super(props); this.onClick = this.onClick.bind(this); diff --git a/public/app/core/components/TagFilter/TagFilter.tsx b/public/app/core/components/TagFilter/TagFilter.tsx index 0b6058f3dd2..84f3e1819cd 100644 --- a/public/app/core/components/TagFilter/TagFilter.tsx +++ b/public/app/core/components/TagFilter/TagFilter.tsx @@ -4,13 +4,13 @@ import { Async } from 'react-select'; import { TagValue } from './TagValue'; import { TagOption } from './TagOption'; -export interface IProps { +export interface Props { tags: string[]; tagOptions: () => any; onSelect: (tag: string) => void; } -export class TagFilter extends React.Component { +export class TagFilter extends React.Component { inlineTags: boolean; constructor(props) { diff --git a/public/app/core/components/TagFilter/TagOption.tsx b/public/app/core/components/TagFilter/TagOption.tsx index 402544dd5f3..5938c98f870 100644 --- a/public/app/core/components/TagFilter/TagOption.tsx +++ b/public/app/core/components/TagFilter/TagOption.tsx @@ -1,7 +1,7 @@ import React from 'react'; import { TagBadge } from './TagBadge'; -export interface IProps { +export interface Props { onSelect: any; onFocus: any; option: any; @@ -9,7 +9,7 @@ export interface IProps { className: any; } -export class TagOption extends React.Component { +export class TagOption extends React.Component { constructor(props) { super(props); this.handleMouseDown = this.handleMouseDown.bind(this); diff --git a/public/app/core/components/TagFilter/TagValue.tsx b/public/app/core/components/TagFilter/TagValue.tsx index 2e7819951f2..ca8ca9e4fba 100644 --- a/public/app/core/components/TagFilter/TagValue.tsx +++ b/public/app/core/components/TagFilter/TagValue.tsx @@ -1,14 +1,14 @@ import React from 'react'; import { TagBadge } from './TagBadge'; -export interface IProps { +export interface Props { value: any; className: any; onClick: any; onRemove: any; } -export class TagValue extends React.Component { +export class TagValue extends React.Component { constructor(props) { super(props); this.onClick = this.onClick.bind(this); diff --git a/public/app/core/components/Tooltip/Popover.tsx b/public/app/core/components/Tooltip/Popover.tsx index 4dc25d34130..ee86d07fb53 100644 --- a/public/app/core/components/Tooltip/Popover.tsx +++ b/public/app/core/components/Tooltip/Popover.tsx @@ -2,11 +2,11 @@ import withTooltip from './withTooltip'; import { Target } from 'react-popper'; -interface IPopoverProps { +interface PopoverProps { tooltipSetState: (prevState: object) => void; } -class Popover extends React.Component { +class Popover extends React.Component { constructor(props) { super(props); this.toggleTooltip = this.toggleTooltip.bind(this); diff --git a/public/app/core/components/Tooltip/Tooltip.tsx b/public/app/core/components/Tooltip/Tooltip.tsx index ae4093ea3f1..a265c8487d3 100644 --- a/public/app/core/components/Tooltip/Tooltip.tsx +++ b/public/app/core/components/Tooltip/Tooltip.tsx @@ -2,11 +2,11 @@ import withTooltip from './withTooltip'; import { Target } from 'react-popper'; -interface ITooltipProps { +interface TooltipProps { tooltipSetState: (prevState: object) => void; } -class Tooltip extends React.Component { +class Tooltip extends React.Component { constructor(props) { super(props); this.showTooltip = this.showTooltip.bind(this); diff --git a/public/app/core/components/colorpicker/ColorPalette.tsx b/public/app/core/components/colorpicker/ColorPalette.tsx index 07b25a32046..edb2629d16d 100644 --- a/public/app/core/components/colorpicker/ColorPalette.tsx +++ b/public/app/core/components/colorpicker/ColorPalette.tsx @@ -1,12 +1,12 @@ import React from 'react'; import { sortedColors } from 'app/core/utils/colors'; -export interface IProps { +export interface Props { color: string; onColorSelect: (c: string) => void; } -export class ColorPalette extends React.Component { +export class ColorPalette extends React.Component { paletteColors: string[]; constructor(props) { @@ -29,7 +29,8 @@ export class ColorPalette extends React.Component { key={paletteColor} className={'pointer fa ' + cssClass} style={{ color: paletteColor }} - onClick={this.onColorSelect(paletteColor)}> + onClick={this.onColorSelect(paletteColor)} + >   ); @@ -41,4 +42,3 @@ export class ColorPalette extends React.Component { ); } } - diff --git a/public/app/core/components/colorpicker/ColorPicker.tsx b/public/app/core/components/colorpicker/ColorPicker.tsx index dbba75636d0..c492d3829ca 100644 --- a/public/app/core/components/colorpicker/ColorPicker.tsx +++ b/public/app/core/components/colorpicker/ColorPicker.tsx @@ -5,12 +5,12 @@ import Drop from 'tether-drop'; import { ColorPickerPopover } from './ColorPickerPopover'; import { react2AngularDirective } from 'app/core/utils/react2angular'; -export interface IProps { +export interface Props { color: string; onChange: (c: string) => void; } -export class ColorPicker extends React.Component { +export class ColorPicker extends React.Component { pickerElem: any; colorPickerDrop: any; diff --git a/public/app/core/components/colorpicker/ColorPickerPopover.tsx b/public/app/core/components/colorpicker/ColorPickerPopover.tsx index 360c3fdd5c4..ac7dd6a2738 100644 --- a/public/app/core/components/colorpicker/ColorPickerPopover.tsx +++ b/public/app/core/components/colorpicker/ColorPickerPopover.tsx @@ -6,12 +6,12 @@ import { SpectrumPicker } from './SpectrumPicker'; const DEFAULT_COLOR = '#000000'; -export interface IProps { +export interface Props { color: string; onColorSelect: (c: string) => void; } -export class ColorPickerPopover extends React.Component { +export class ColorPickerPopover extends React.Component { pickerNavElem: any; constructor(props) { @@ -19,7 +19,7 @@ export class ColorPickerPopover extends React.Component { this.state = { tab: 'palette', color: this.props.color || DEFAULT_COLOR, - colorString: this.props.color || DEFAULT_COLOR + colorString: this.props.color || DEFAULT_COLOR, }; } @@ -32,7 +32,7 @@ export class ColorPickerPopover extends React.Component { if (newColor.isValid()) { this.setState({ color: newColor.toString(), - colorString: newColor.toString() + colorString: newColor.toString(), }); this.props.onColorSelect(color); } @@ -50,7 +50,7 @@ export class ColorPickerPopover extends React.Component { onColorStringChange(e) { let colorString = e.target.value; this.setState({ - colorString: colorString + colorString: colorString, }); let newColor = tinycolor(colorString); @@ -71,11 +71,11 @@ export class ColorPickerPopover extends React.Component { componentDidMount() { this.pickerNavElem.find('li:first').addClass('active'); - this.pickerNavElem.on('show', (e) => { + this.pickerNavElem.on('show', e => { // use href attr (#name => name) let tab = e.target.hash.slice(1); this.setState({ - tab: tab + tab: tab, }); }); } @@ -97,19 +97,24 @@ export class ColorPickerPopover extends React.Component {
    -
    - {currentTab} -
    +
    {currentTab}
    - - +
    ); diff --git a/public/app/core/components/colorpicker/SeriesColorPicker.tsx b/public/app/core/components/colorpicker/SeriesColorPicker.tsx index 3b24b9a4661..b514899e2e2 100644 --- a/public/app/core/components/colorpicker/SeriesColorPicker.tsx +++ b/public/app/core/components/colorpicker/SeriesColorPicker.tsx @@ -2,13 +2,13 @@ import React from 'react'; import { ColorPickerPopover } from './ColorPickerPopover'; import { react2AngularDirective } from 'app/core/utils/react2angular'; -export interface IProps { +export interface Props { series: any; onColorChange: (color: string) => void; onToggleAxis: () => void; } -export class SeriesColorPicker extends React.Component { +export class SeriesColorPicker extends React.Component { constructor(props) { super(props); this.onColorChange = this.onColorChange.bind(this); diff --git a/public/app/core/components/colorpicker/SpectrumPicker.tsx b/public/app/core/components/colorpicker/SpectrumPicker.tsx index eef04545308..e8a30e8c460 100644 --- a/public/app/core/components/colorpicker/SpectrumPicker.tsx +++ b/public/app/core/components/colorpicker/SpectrumPicker.tsx @@ -3,13 +3,13 @@ import _ from 'lodash'; import $ from 'jquery'; import 'vendor/spectrum'; -export interface IProps { +export interface Props { color: string; options: object; onColorSelect: (c: string) => void; } -export class SpectrumPicker extends React.Component { +export class SpectrumPicker extends React.Component { elem: any; isMoving: boolean; @@ -29,14 +29,17 @@ export class SpectrumPicker extends React.Component { } componentDidMount() { - let spectrumOptions = _.assignIn({ - flat: true, - showAlpha: true, - showButtons: false, - color: this.props.color, - appendTo: this.elem, - move: this.onSpectrumMove, - }, this.props.options); + let spectrumOptions = _.assignIn( + { + flat: true, + showAlpha: true, + showButtons: false, + color: this.props.color, + appendTo: this.elem, + move: this.onSpectrumMove, + }, + this.props.options + ); this.elem.spectrum(spectrumOptions); this.elem.spectrum('show'); @@ -64,9 +67,6 @@ export class SpectrumPicker extends React.Component { } render() { - return ( -
    - ); + return
    ; } } - diff --git a/public/app/stores/AlertListStore/AlertListStore.ts b/public/app/stores/AlertListStore/AlertListStore.ts index 7d60ce04180..ec27565a1a1 100644 --- a/public/app/stores/AlertListStore/AlertListStore.ts +++ b/public/app/stores/AlertListStore/AlertListStore.ts @@ -1,13 +1,13 @@ import { types, getEnv, flow } from 'mobx-state-tree'; -import { AlertRule } from './AlertRule'; +import { AlertRule as AlertRuleModel } from './AlertRule'; import { setStateFields } from './helpers'; -type IAlertRuleType = typeof AlertRule.Type; -export interface IAlertRule extends IAlertRuleType {} +type AlertRuleType = typeof AlertRuleModel.Type; +export interface AlertRule extends AlertRuleType {} export const AlertListStore = types .model('AlertListStore', { - rules: types.array(AlertRule), + rules: types.array(AlertRuleModel), stateFilter: types.optional(types.string, 'all'), search: types.optional(types.string, ''), }) @@ -38,7 +38,7 @@ export const AlertListStore = types } } - self.rules.push(AlertRule.create(rule)); + self.rules.push(AlertRuleModel.create(rule)); } }), setSearchQuery(query: string) { diff --git a/public/app/stores/NavStore/NavStore.ts b/public/app/stores/NavStore/NavStore.ts index c69c32befa8..bef53b828b6 100644 --- a/public/app/stores/NavStore/NavStore.ts +++ b/public/app/stores/NavStore/NavStore.ts @@ -1,7 +1,7 @@ import _ from 'lodash'; import { types, getEnv } from 'mobx-state-tree'; import { NavItem } from './NavItem'; -import { ITeam } from '../TeamsStore/TeamsStore'; +import { Team } from '../TeamsStore/TeamsStore'; export const NavStore = types .model('NavStore', { @@ -117,7 +117,7 @@ export const NavStore = types self.main = NavItem.create(main); }, - initTeamPage(team: ITeam, tab: string, isSyncEnabled: boolean) { + initTeamPage(team: Team, tab: string, isSyncEnabled: boolean) { let main = { img: team.avatarUrl, id: 'team-' + team.id, diff --git a/public/app/stores/RootStore/RootStore.ts b/public/app/stores/RootStore/RootStore.ts index 8a915d20ef1..bb85a85d9dd 100644 --- a/public/app/stores/RootStore/RootStore.ts +++ b/public/app/stores/RootStore/RootStore.ts @@ -34,5 +34,5 @@ export const RootStore = types.model({ }), }); -type IRootStoreType = typeof RootStore.Type; -export interface IRootStore extends IRootStoreType {} +type RootStoreType = typeof RootStore.Type; +export interface RootStoreInterface extends RootStoreType {} diff --git a/public/app/stores/TeamsStore/TeamsStore.ts b/public/app/stores/TeamsStore/TeamsStore.ts index 01cdca895d4..1aec4a1433c 100644 --- a/public/app/stores/TeamsStore/TeamsStore.ts +++ b/public/app/stores/TeamsStore/TeamsStore.ts @@ -1,6 +1,6 @@ import { types, getEnv, flow } from 'mobx-state-tree'; -export const TeamMember = types.model('TeamMember', { +export const TeamMemberModel = types.model('TeamMember', { userId: types.identifier(types.number), teamId: types.number, avatarUrl: types.string, @@ -8,18 +8,18 @@ export const TeamMember = types.model('TeamMember', { login: types.string, }); -type TeamMemberType = typeof TeamMember.Type; -export interface ITeamMember extends TeamMemberType {} +type TeamMemberType = typeof TeamMemberModel.Type; +export interface TeamMember extends TeamMemberType {} -export const TeamGroup = types.model('TeamGroup', { +export const TeamGroupModel = types.model('TeamGroup', { groupId: types.identifier(types.string), teamId: types.number, }); -type TeamGroupType = typeof TeamGroup.Type; -export interface ITeamGroup extends TeamGroupType {} +type TeamGroupType = typeof TeamGroupModel.Type; +export interface TeamGroup extends TeamGroupType {} -export const Team = types +export const TeamModel = types .model('Team', { id: types.identifier(types.number), name: types.string, @@ -27,8 +27,8 @@ export const Team = types email: types.string, memberCount: types.number, search: types.optional(types.string, ''), - members: types.optional(types.map(TeamMember), {}), - groups: types.optional(types.map(TeamGroup), {}), + members: types.optional(types.map(TeamMemberModel), {}), + groups: types.optional(types.map(TeamGroupModel), {}), }) .views(self => ({ get filteredMembers() { @@ -67,11 +67,11 @@ export const Team = types self.members.clear(); for (let member of rsp) { - self.members.set(member.userId.toString(), TeamMember.create(member)); + self.members.set(member.userId.toString(), TeamMemberModel.create(member)); } }), - removeMember: flow(function* load(member: ITeamMember) { + removeMember: flow(function* load(member: TeamMember) { const backendSrv = getEnv(self).backendSrv; yield backendSrv.delete(`/api/teams/${self.id}/members/${member.userId}`); // remove from store map @@ -89,7 +89,7 @@ export const Team = types self.groups.clear(); for (let group of rsp) { - self.groups.set(group.groupId, TeamGroup.create(group)); + self.groups.set(group.groupId, TeamGroupModel.create(group)); } }), @@ -98,7 +98,7 @@ export const Team = types yield backendSrv.post(`/api/teams/${self.id}/groups`, { groupId: groupId }); self.groups.set( groupId, - TeamGroup.create({ + TeamGroupModel.create({ teamId: self.id, groupId: groupId, }) @@ -112,12 +112,12 @@ export const Team = types }), })); -type TeamType = typeof Team.Type; -export interface ITeam extends TeamType {} +type TeamType = typeof TeamModel.Type; +export interface Team extends TeamType {} export const TeamsStore = types .model('TeamsStore', { - map: types.map(Team), + map: types.map(TeamModel), search: types.optional(types.string, ''), }) .views(self => ({ @@ -136,7 +136,7 @@ export const TeamsStore = types self.map.clear(); for (let team of rsp.teams) { - self.map.set(team.id.toString(), Team.create(team)); + self.map.set(team.id.toString(), TeamModel.create(team)); } }), @@ -151,6 +151,6 @@ export const TeamsStore = types const backendSrv = getEnv(self).backendSrv; const team = yield backendSrv.get(`/api/teams/${id}`); - self.map.set(id, Team.create(team)); + self.map.set(id, TeamModel.create(team)); }), })); diff --git a/public/app/stores/store.ts b/public/app/stores/store.ts index dfbd8141198..10acbfe4907 100644 --- a/public/app/stores/store.ts +++ b/public/app/stores/store.ts @@ -1,7 +1,7 @@ -import { RootStore, IRootStore } from './RootStore/RootStore'; +import { RootStore, RootStoreInterface } from './RootStore/RootStore'; import config from 'app/core/config'; -export let store: IRootStore; +export let store: RootStoreInterface; export function createStore(services) { store = RootStore.create( diff --git a/tslint.json b/tslint.json index 22e123e0364..9a72f9ccebc 100644 --- a/tslint.json +++ b/tslint.json @@ -1,5 +1,6 @@ { "rules": { + "interface-name": [true, "never-prefix"], "no-string-throw": true, "no-unused-expression": true, "no-unused-variable": false, diff --git a/yarn.lock b/yarn.lock index dd1cde4e698..fb593043288 100644 --- a/yarn.lock +++ b/yarn.lock @@ -11454,6 +11454,12 @@ tslint-loader@^3.5.3: rimraf "^2.4.4" semver "^5.3.0" +tslint-react@^3.6.0: + version "3.6.0" + resolved "https://registry.yarnpkg.com/tslint-react/-/tslint-react-3.6.0.tgz#7f462c95c4a0afaae82507f06517ff02942196a1" + dependencies: + tsutils "^2.13.1" + tslint@^5.8.0: version "5.10.0" resolved "https://registry.yarnpkg.com/tslint/-/tslint-5.10.0.tgz#11e26bccb88afa02dd0d9956cae3d4540b5f54c3" @@ -11477,6 +11483,12 @@ tsutils@^2.12.1: dependencies: tslib "^1.8.1" +tsutils@^2.13.1: + version "2.29.0" + resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-2.29.0.tgz#32b488501467acbedd4b85498673a0812aca0b99" + dependencies: + tslib "^1.8.1" + tty-browserify@0.0.0: version "0.0.0" resolved "https://registry.yarnpkg.com/tty-browserify/-/tty-browserify-0.0.0.tgz#a157ba402da24e9bf957f9aa69d524eed42901a6" From eba147c1a3f45f0b76399b87a288a612f240bfbf Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Fri, 24 Aug 2018 19:09:19 +0200 Subject: [PATCH 503/786] change/add tests for alerting notification reminders --- .../sqlstore/alert_notification_test.go | 21 +++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/pkg/services/sqlstore/alert_notification_test.go b/pkg/services/sqlstore/alert_notification_test.go index aba437f427e..83fb42db9bb 100644 --- a/pkg/services/sqlstore/alert_notification_test.go +++ b/pkg/services/sqlstore/alert_notification_test.go @@ -3,6 +3,7 @@ package sqlstore import ( "context" "testing" + "time" "github.com/grafana/grafana/pkg/components/simplejson" m "github.com/grafana/grafana/pkg/models" @@ -88,7 +89,7 @@ func TestAlertNotificationSQLAccess(t *testing.T) { }) }) - Convey("Cannot update alert notifier with notitfyonce = false", func() { + Convey("Cannot update alert notifier with send reminder = false", func() { cmd := &m.CreateAlertNotificationCommand{ Name: "ops update", Type: "email", @@ -134,6 +135,7 @@ func TestAlertNotificationSQLAccess(t *testing.T) { So(cmd.Result.Id, ShouldNotEqual, 0) So(cmd.Result.OrgId, ShouldNotEqual, 0) So(cmd.Result.Type, ShouldEqual, "email") + So(cmd.Result.Frequency, ShouldEqual, 10*time.Second) Convey("Cannot save Alert Notification with the same name", func() { err = CreateAlertNotificationCommand(cmd) @@ -146,13 +148,28 @@ func TestAlertNotificationSQLAccess(t *testing.T) { Type: "webhook", OrgId: cmd.Result.OrgId, SendReminder: true, - Frequency: "10s", + Frequency: "60s", Settings: simplejson.New(), Id: cmd.Result.Id, } err := UpdateAlertNotification(newCmd) So(err, ShouldBeNil) So(newCmd.Result.Name, ShouldEqual, "NewName") + So(newCmd.Result.Frequency, ShouldEqual, 60*time.Second) + }) + + Convey("Can update alert notification to disable sending of reminders", func() { + newCmd := &m.UpdateAlertNotificationCommand{ + Name: "NewName", + Type: "webhook", + OrgId: cmd.Result.OrgId, + SendReminder: false, + Settings: simplejson.New(), + Id: cmd.Result.Id, + } + err := UpdateAlertNotification(newCmd) + So(err, ShouldBeNil) + So(newCmd.Result.SendReminder, ShouldBeFalse) }) }) From 6995242b8b2db85fd934c2cdedb15a9c0797de85 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Fri, 24 Aug 2018 19:13:52 +0200 Subject: [PATCH 504/786] copy and docs update for alert notification reminders --- docs/sources/alerting/notifications.md | 2 +- public/app/features/alerting/partials/notification_edit.html | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/sources/alerting/notifications.md b/docs/sources/alerting/notifications.md index 5262cc2bc48..a5b7f4264e0 100644 --- a/docs/sources/alerting/notifications.md +++ b/docs/sources/alerting/notifications.md @@ -40,7 +40,7 @@ When checked, this option will notify for all alert rules - existing and new. When this option is checked additional notifications (reminders) will be sent for triggered alerts. You can specify how often reminders should be sent using number of seconds (s), minutes (m) or hours (h), for example `30s`, `3m`, `5m` or `1h` etc. -**Important:** Alert reminders are sent after rules are evaluated. Therefore a reminder can never be sent earlier than a configured [alert rule evaluation interval](/alerting/rules/#name-evaluation-interval). +**Important:** Alert reminders are sent after rules are evaluated. Therefore a reminder can never be sent more frequently than a configured [alert rule evaluation interval](/alerting/rules/#name-evaluation-interval). These examples show how often and when reminders are sent for a triggered alert. diff --git a/public/app/features/alerting/partials/notification_edit.html b/public/app/features/alerting/partials/notification_edit.html index faa168d0acd..7b198736b83 100644 --- a/public/app/features/alerting/partials/notification_edit.html +++ b/public/app/features/alerting/partials/notification_edit.html @@ -52,7 +52,7 @@
    - Alert reminders are sent after rules are evaluated. Therefore a reminder can never be sent earlier than a configured alert rule evaluation interval. + Alert reminders are sent after rules are evaluated. Therefore a reminder can never be sent more frequently than a configured alert rule evaluation interval.
    From 21e7b0b92d3534c3059f7fad47364a269276ff1e Mon Sep 17 00:00:00 2001 From: Sven Klemm Date: Sat, 25 Aug 2018 18:14:39 +0200 Subject: [PATCH 505/786] add min interval to postgres datasource --- .../plugins/datasource/postgres/partials/config.html | 12 ++++++++++++ public/app/plugins/datasource/postgres/plugin.json | 7 ++++++- public/app/plugins/datasource/postgres/query_ctrl.ts | 2 +- 3 files changed, 19 insertions(+), 2 deletions(-) diff --git a/public/app/plugins/datasource/postgres/partials/config.html b/public/app/plugins/datasource/postgres/partials/config.html index 40b0f63d254..4cb0cefba9e 100644 --- a/public/app/plugins/datasource/postgres/partials/config.html +++ b/public/app/plugins/datasource/postgres/partials/config.html @@ -38,6 +38,18 @@ +
    +
    +
    + Min time interval + + + A lower limit for the auto group by time interval. Recommended to be set to write frequency, + for example 1m if your data is written every minute. + +
    +
    +

    PostgreSQL details

    diff --git a/public/app/plugins/datasource/postgres/plugin.json b/public/app/plugins/datasource/postgres/plugin.json index af2dbc4468e..f236aa01b06 100644 --- a/public/app/plugins/datasource/postgres/plugin.json +++ b/public/app/plugins/datasource/postgres/plugin.json @@ -18,5 +18,10 @@ "alerting": true, "annotations": true, - "metrics": true + "metrics": true, + + "queryOptions": { + "minInterval": true + } + } diff --git a/public/app/plugins/datasource/postgres/query_ctrl.ts b/public/app/plugins/datasource/postgres/query_ctrl.ts index b6850112446..057086e1cd1 100644 --- a/public/app/plugins/datasource/postgres/query_ctrl.ts +++ b/public/app/plugins/datasource/postgres/query_ctrl.ts @@ -317,7 +317,7 @@ export class PostgresQueryCtrl extends QueryCtrl { case 'aggregate': // add group by if no group by yet if (this.target.group.length === 0) { - this.addGroup('time', '1m'); + this.addGroup('time', '$__interval'); } let aggIndex = this.findAggregateIndex(selectParts); if (aggIndex !== -1) { From fda9790ba5b1a143eea59187d7c651ec00b7de53 Mon Sep 17 00:00:00 2001 From: bergquist Date: Sat, 25 Aug 2018 21:23:20 +0200 Subject: [PATCH 506/786] upgrades to golang 1.11 --- .circleci/config.yml | 8 ++++---- Dockerfile | 2 +- README.md | 2 +- appveyor.yml | 2 +- docs/sources/project/building_from_source.md | 2 +- scripts/build/Dockerfile | 2 +- 6 files changed, 9 insertions(+), 9 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 1e046aec34d..b4480b4bade 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -19,7 +19,7 @@ version: 2 jobs: mysql-integration-test: docker: - - image: circleci/golang:1.10 + - image: circleci/golang:1.11 - image: circleci/mysql:5.6-ram environment: MYSQL_ROOT_PASSWORD: rootpass @@ -39,7 +39,7 @@ jobs: postgres-integration-test: docker: - - image: circleci/golang:1.10 + - image: circleci/golang:1.11 - image: circleci/postgres:9.3-ram environment: POSTGRES_USER: grafanatest @@ -74,7 +74,7 @@ jobs: gometalinter: docker: - - image: circleci/golang:1.10 + - image: circleci/golang:1.11 environment: # we need CGO because of go-sqlite3 CGO_ENABLED: 1 @@ -115,7 +115,7 @@ jobs: test-backend: docker: - - image: circleci/golang:1.10 + - image: circleci/golang:1.11 working_directory: /go/src/github.com/grafana/grafana steps: - checkout diff --git a/Dockerfile b/Dockerfile index f7e45893c38..28dd71952af 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ # Golang build container -FROM golang:1.10 +FROM golang:1.11 WORKDIR $GOPATH/src/github.com/grafana/grafana diff --git a/README.md b/README.md index 74fb10c8066..133d9e50d07 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ the latest master builds [here](https://grafana.com/grafana/download) ### Dependencies -- Go 1.10 +- Go 1.11 - NodeJS LTS ### Building the backend diff --git a/appveyor.yml b/appveyor.yml index 5cdec1b8bf5..52f23162033 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -7,7 +7,7 @@ clone_folder: c:\gopath\src\github.com\grafana\grafana environment: nodejs_version: "6" GOPATH: C:\gopath - GOVERSION: 1.10 + GOVERSION: 1.11 install: - rmdir c:\go /s /q diff --git a/docs/sources/project/building_from_source.md b/docs/sources/project/building_from_source.md index 08673404572..e83c62ca800 100644 --- a/docs/sources/project/building_from_source.md +++ b/docs/sources/project/building_from_source.md @@ -13,7 +13,7 @@ dev environment. Grafana ships with its own required backend server; also comple ## Dependencies -- [Go 1.10](https://golang.org/dl/) +- [Go 1.11](https://golang.org/dl/) - [Git](https://git-scm.com/downloads) - [NodeJS LTS](https://nodejs.org/download/) - node-gyp is the Node.js native addon build tool and it requires extra dependencies: python 2.7, make and GCC. These are already installed for most Linux distros and MacOS. See the Building On Windows section or the [node-gyp installation instructions](https://github.com/nodejs/node-gyp#installation) for more details. diff --git a/scripts/build/Dockerfile b/scripts/build/Dockerfile index 808e7f141e9..c7f4fecc649 100644 --- a/scripts/build/Dockerfile +++ b/scripts/build/Dockerfile @@ -21,7 +21,7 @@ RUN gpg --keyserver hkp://keys.gnupg.net --recv-keys 409B6B1796C275462A170311380 RUN curl --silent --location https://rpm.nodesource.com/setup_6.x | bash - && \ yum install -y nodejs --nogpgcheck -ENV GOLANG_VERSION 1.10 +ENV GOLANG_VERSION 1.11 RUN wget https://dl.yarnpkg.com/rpm/yarn.repo -O /etc/yum.repos.d/yarn.repo && \ yum install -y yarn --nogpgcheck && \ From 9b978b7203afdde901fa0d4324719b2aa64db271 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Sun, 26 Aug 2018 17:14:40 +0200 Subject: [PATCH 507/786] tslint: autofix of let -> const (#13033) --- .../AlertRuleList/AlertRuleList.test.tsx | 2 +- .../AlertRuleList/AlertRuleList.tsx | 4 +- public/app/containers/Explore/Table.tsx | 2 +- .../containers/Explore/utils/prometheus.ts | 2 +- public/app/containers/Teams/TeamList.tsx | 2 +- .../Permissions/AddPermissions.test.tsx | 2 +- .../core/components/TagFilter/TagFilter.tsx | 2 +- .../components/code_editor/code_editor.ts | 30 ++-- .../components/colorpicker/ColorPicker.tsx | 4 +- .../colorpicker/ColorPickerPopover.tsx | 14 +- .../components/colorpicker/SpectrumPicker.tsx | 2 +- .../components/form_dropdown/form_dropdown.ts | 2 +- public/app/core/components/grafana_app.ts | 2 +- public/app/core/components/info_popover.ts | 14 +- .../manage_dashboards/manage_dashboards.ts | 14 +- public/app/core/components/scroll/scroll.ts | 6 +- .../core/components/search/SearchResult.tsx | 2 +- .../app/core/components/sidemenu/sidemenu.ts | 4 +- public/app/core/controllers/login_ctrl.ts | 4 +- .../app/core/directives/dropdown_typeahead.ts | 32 ++--- public/app/core/directives/metric_segment.ts | 28 ++-- public/app/core/directives/misc.ts | 2 +- .../core/directives/value_select_dropdown.ts | 18 +-- public/app/core/nav_model_srv.ts | 6 +- public/app/core/services/backend_srv.ts | 10 +- public/app/core/services/bridge_srv.ts | 4 +- .../core/services/dynamic_directive_srv.ts | 2 +- public/app/core/services/keybindingSrv.ts | 2 +- public/app/core/services/ng_react.ts | 2 +- public/app/core/services/search_srv.ts | 14 +- public/app/core/services/segment_srv.ts | 4 +- public/app/core/specs/backend_srv.test.ts | 4 +- public/app/core/specs/file_export.test.ts | 6 +- public/app/core/specs/search.test.ts | 2 +- public/app/core/specs/search_results.test.ts | 8 +- public/app/core/specs/ticks.test.ts | 2 +- public/app/core/specs/time_series.test.ts | 10 +- .../core/specs/value_select_dropdown.test.ts | 2 +- public/app/core/time_series2.ts | 8 +- public/app/core/utils/colors.ts | 4 +- public/app/core/utils/dag.test.ts | 20 +-- public/app/core/utils/dag.ts | 18 +-- public/app/core/utils/file_export.ts | 16 +-- public/app/core/utils/outline.ts | 2 +- public/app/core/utils/rangeutil.ts | 10 +- public/app/core/utils/sort_by_keys.ts | 2 +- public/app/core/utils/tags.ts | 6 +- public/app/core/utils/ticks.ts | 22 +-- public/app/core/utils/url.ts | 10 +- public/app/core/utils/version.ts | 6 +- .../app/features/alerting/alert_tab_ctrl.ts | 4 +- .../alerting/notification_edit_ctrl.ts | 2 +- .../app/features/alerting/threshold_mapper.ts | 14 +- .../features/annotations/annotations_srv.ts | 2 +- .../app/features/annotations/event_editor.ts | 4 +- .../app/features/annotations/event_manager.ts | 12 +- .../features/annotations/events_processing.ts | 12 +- .../annotations/specs/annotations_srv.test.ts | 4 +- .../specs/annotations_srv_specs.test.ts | 8 +- .../app/features/dashboard/ad_hoc_filters.ts | 2 +- .../app/features/dashboard/change_tracker.ts | 8 +- .../dashboard/dashboard_import_ctrl.ts | 4 +- .../features/dashboard/dashboard_migration.ts | 14 +- .../app/features/dashboard/dashboard_model.ts | 88 ++++++------ .../dashboard/dashgrid/AddPanelPanel.tsx | 20 +-- .../dashboard/dashgrid/DashboardGrid.tsx | 8 +- .../app/features/dashboard/dashnav/dashnav.ts | 4 +- .../features/dashboard/export/export_modal.ts | 2 +- .../app/features/dashboard/export/exporter.ts | 14 +- .../app/features/dashboard/history/history.ts | 4 +- .../features/dashboard/settings/settings.ts | 2 +- .../app/features/dashboard/shareModalCtrl.ts | 2 +- .../specs/dashboard_migration.test.ts | 84 +++++------ .../dashboard/specs/dashboard_model.test.ts | 32 ++--- .../dashboard/specs/history_ctrl.test.ts | 4 +- .../dashboard/specs/history_srv.test.ts | 6 +- .../features/dashboard/specs/repeat.test.ts | 2 +- .../dashboard/specs/viewstate_srv.test.ts | 6 +- .../app/features/dashboard/validation_srv.ts | 4 +- .../app/features/dashboard/view_state_srv.ts | 4 +- public/app/features/org/org_users_ctrl.ts | 2 +- public/app/features/panel/metrics_tab.ts | 2 +- public/app/features/panel/panel_ctrl.ts | 16 +-- public/app/features/panel/panel_directive.ts | 4 +- public/app/features/panel/panel_header.ts | 12 +- public/app/features/panel/solo_panel_ctrl.ts | 2 +- .../panellinks/specs/link_srv.test.ts | 2 +- .../app/features/playlist/playlist_routes.ts | 2 +- .../playlist/specs/playlist_edit_ctrl.test.ts | 2 +- public/app/features/plugins/ds_list_ctrl.ts | 2 +- .../app/features/plugins/plugin_component.ts | 8 +- .../app/features/plugins/plugin_edit_ctrl.ts | 6 +- .../app/features/plugins/plugin_list_ctrl.ts | 2 +- public/app/features/plugins/plugin_loader.ts | 4 +- .../app/features/plugins/plugin_page_ctrl.ts | 2 +- .../plugins/specs/datasource_srv.test.ts | 4 +- .../templating/specs/editor_ctrl.test.ts | 2 +- .../specs/variable_srv_init.test.ts | 6 +- .../app/features/templating/variable_srv.ts | 12 +- .../datasource/cloudwatch/datasource.ts | 4 +- .../cloudwatch/specs/datasource.test.ts | 26 ++-- .../datasource/elasticsearch/datasource.ts | 8 +- .../elasticsearch/elastic_response.ts | 14 +- .../elasticsearch/specs/datasource.test.ts | 10 +- .../plugins/datasource/grafana/datasource.ts | 2 +- .../plugins/datasource/graphite/datasource.ts | 32 ++--- .../datasource/graphite/graphite_query.ts | 18 +-- .../plugins/datasource/graphite/query_ctrl.ts | 30 ++-- .../graphite/specs/datasource.test.ts | 30 ++-- .../graphite/specs/graphite_query.test.ts | 2 +- .../graphite/specs/query_ctrl.test.ts | 2 +- .../plugins/datasource/influxdb/datasource.ts | 12 +- .../datasource/influxdb/influx_query.ts | 4 +- .../plugins/datasource/influxdb/query_ctrl.ts | 6 +- .../influxdb/specs/datasource.test.ts | 6 +- .../influxdb/specs/query_ctrl.test.ts | 2 +- .../plugins/datasource/mssql/query_ctrl.ts | 4 +- .../datasource/mssql/response_parser.ts | 8 +- .../plugins/datasource/mysql/query_ctrl.ts | 4 +- .../datasource/mysql/response_parser.ts | 8 +- .../datasource/mysql/specs/datasource.test.ts | 8 +- .../opentsdb/specs/datasource.test.ts | 4 +- .../plugins/datasource/postgres/query_ctrl.ts | 4 +- .../datasource/postgres/response_parser.ts | 10 +- .../postgres/specs/datasource.test.ts | 8 +- .../datasource/prometheus/completer.ts | 20 +-- .../datasource/prometheus/datasource.ts | 18 +-- .../prometheus/result_transformer.ts | 16 +-- .../prometheus/specs/completer.test.ts | 10 +- .../prometheus/specs/datasource.test.ts | 84 +++++------ .../specs/metric_find_query.test.ts | 4 +- .../specs/result_transformer.test.ts | 10 +- .../plugins/datasource/testdata/datasource.ts | 2 +- public/app/plugins/panel/alertlist/module.ts | 4 +- .../app/plugins/panel/graph/data_processor.ts | 12 +- public/app/plugins/panel/graph/graph.ts | 24 ++-- .../app/plugins/panel/graph/graph_tooltip.ts | 32 ++--- public/app/plugins/panel/graph/histogram.ts | 20 +-- .../plugins/panel/graph/jquery.flot.events.ts | 90 ++++++------ public/app/plugins/panel/graph/legend.ts | 14 +- public/app/plugins/panel/graph/module.ts | 4 +- .../plugins/panel/graph/specs/graph.test.ts | 4 +- .../panel/graph/specs/graph_ctrl.test.ts | 6 +- .../panel/graph/specs/histogram.test.ts | 16 +-- .../graph/specs/series_override_ctrl.test.ts | 2 +- .../app/plugins/panel/heatmap/color_legend.ts | 118 +++++++-------- .../app/plugins/panel/heatmap/color_scale.ts | 8 +- .../app/plugins/panel/heatmap/heatmap_ctrl.ts | 50 +++---- .../panel/heatmap/heatmap_data_converter.ts | 78 +++++----- .../plugins/panel/heatmap/heatmap_tooltip.ts | 52 +++---- public/app/plugins/panel/heatmap/rendering.ts | 134 +++++++++--------- .../panel/heatmap/specs/heatmap_ctrl.test.ts | 6 +- .../specs/heatmap_data_converter.test.ts | 40 +++--- public/app/plugins/panel/pluginlist/module.ts | 2 +- public/app/plugins/panel/singlestat/module.ts | 14 +- .../panel/singlestat/specs/singlestat.test.ts | 8 +- public/app/plugins/panel/table/module.ts | 2 +- public/app/plugins/panel/table/renderer.ts | 26 ++-- .../app/plugins/panel/table/transformers.ts | 2 +- .../stores/AlertListStore/AlertListStore.ts | 4 +- public/app/stores/NavStore/NavStore.ts | 14 +- .../PermissionsStore/PermissionsStore.ts | 6 +- public/app/stores/TeamsStore/TeamsStore.ts | 14 +- public/app/stores/ViewStore/ViewStore.ts | 4 +- public/test/core/utils/version_test.ts | 42 +++--- public/test/index.ts | 6 +- public/test/mocks/common.ts | 6 +- 167 files changed, 1077 insertions(+), 1081 deletions(-) diff --git a/public/app/containers/AlertRuleList/AlertRuleList.test.tsx b/public/app/containers/AlertRuleList/AlertRuleList.test.tsx index eac18a6c69d..f88ff4522d4 100644 --- a/public/app/containers/AlertRuleList/AlertRuleList.test.tsx +++ b/public/app/containers/AlertRuleList/AlertRuleList.test.tsx @@ -46,7 +46,7 @@ describe('AlertRuleList', () => { it('should render 1 rule', () => { page.update(); - let ruleNode = page.find('.alert-rule-item'); + const ruleNode = page.find('.alert-rule-item'); expect(toJson(ruleNode)).toMatchSnapshot(); }); diff --git a/public/app/containers/AlertRuleList/AlertRuleList.tsx b/public/app/containers/AlertRuleList/AlertRuleList.tsx index 3c2da77c2a7..668136dee6f 100644 --- a/public/app/containers/AlertRuleList/AlertRuleList.tsx +++ b/public/app/containers/AlertRuleList/AlertRuleList.tsx @@ -132,13 +132,13 @@ export class AlertRuleItem extends React.Component { render() { const { rule } = this.props; - let stateClass = classNames({ + const stateClass = classNames({ fa: true, 'fa-play': rule.isPaused, 'fa-pause': !rule.isPaused, }); - let ruleUrl = `${rule.url}?panelId=${rule.panelId}&fullscreen=true&edit=true&tab=alert`; + const ruleUrl = `${rule.url}?panelId=${rule.panelId}&fullscreen=true&edit=true&tab=alert`; return (
  • diff --git a/public/app/containers/Explore/Table.tsx b/public/app/containers/Explore/Table.tsx index e5adde2d008..cbb3ab11f4e 100644 --- a/public/app/containers/Explore/Table.tsx +++ b/public/app/containers/Explore/Table.tsx @@ -40,7 +40,7 @@ function Cell(props: SFCCellProps) { export default class Table extends PureComponent { render() { const { className = '', data, loading, onClickCell } = this.props; - let tableModel = data || EMPTY_TABLE; + const tableModel = data || EMPTY_TABLE; if (!loading && data && data.rows.length === 0) { return ( diff --git a/public/app/containers/Explore/utils/prometheus.ts b/public/app/containers/Explore/utils/prometheus.ts index f5ccb848f2f..19129976282 100644 --- a/public/app/containers/Explore/utils/prometheus.ts +++ b/public/app/containers/Explore/utils/prometheus.ts @@ -65,7 +65,7 @@ export function parseSelector(query: string, cursorOffset = 1): { labelKeys: any // Extract clean labels to form clean selector, incomplete labels are dropped const selector = query.slice(prefixOpen, suffixClose); - let labels = {}; + const labels = {}; selector.replace(labelRegexp, match => { const delimiterIndex = match.indexOf('='); const key = match.slice(0, delimiterIndex); diff --git a/public/app/containers/Teams/TeamList.tsx b/public/app/containers/Teams/TeamList.tsx index 2d037eed642..d0feee75184 100644 --- a/public/app/containers/Teams/TeamList.tsx +++ b/public/app/containers/Teams/TeamList.tsx @@ -36,7 +36,7 @@ export class TeamList extends React.Component { }; renderTeamMember(team: Team): JSX.Element { - let teamUrl = `org/teams/edit/${team.id}`; + const teamUrl = `org/teams/edit/${team.id}`; return ( diff --git a/public/app/core/components/Permissions/AddPermissions.test.tsx b/public/app/core/components/Permissions/AddPermissions.test.tsx index 513a22ddea4..c6d1ab381b8 100644 --- a/public/app/core/components/Permissions/AddPermissions.test.tsx +++ b/public/app/core/components/Permissions/AddPermissions.test.tsx @@ -22,7 +22,7 @@ describe('AddPermissions', () => { let wrapper; let store; let instance; - let backendSrv: any = getBackendSrv(); + const backendSrv: any = getBackendSrv(); beforeAll(() => { store = RootStore.create({}, { backendSrv: backendSrv }); diff --git a/public/app/core/components/TagFilter/TagFilter.tsx b/public/app/core/components/TagFilter/TagFilter.tsx index 84f3e1819cd..a879f544da0 100644 --- a/public/app/core/components/TagFilter/TagFilter.tsx +++ b/public/app/core/components/TagFilter/TagFilter.tsx @@ -43,7 +43,7 @@ export class TagFilter extends React.Component { } render() { - let selectOptions = { + const selectOptions = { loadOptions: this.searchTags, onChange: this.onChange, value: this.props.tags, diff --git a/public/app/core/components/code_editor/code_editor.ts b/public/app/core/components/code_editor/code_editor.ts index 886ae2a6407..66aec778d73 100644 --- a/public/app/core/components/code_editor/code_editor.ts +++ b/public/app/core/components/code_editor/code_editor.ts @@ -53,23 +53,23 @@ const DEFAULT_TAB_SIZE = 2; const DEFAULT_BEHAVIOURS = true; const DEFAULT_SNIPPETS = true; -let editorTemplate = `
    `; +const editorTemplate = `
    `; function link(scope, elem, attrs) { // Options - let langMode = attrs.mode || DEFAULT_MODE; - let maxLines = attrs.maxLines || DEFAULT_MAX_LINES; - let showGutter = attrs.showGutter !== undefined; - let tabSize = attrs.tabSize || DEFAULT_TAB_SIZE; - let behavioursEnabled = attrs.behavioursEnabled ? attrs.behavioursEnabled === 'true' : DEFAULT_BEHAVIOURS; - let snippetsEnabled = attrs.snippetsEnabled ? attrs.snippetsEnabled === 'true' : DEFAULT_SNIPPETS; + const langMode = attrs.mode || DEFAULT_MODE; + const maxLines = attrs.maxLines || DEFAULT_MAX_LINES; + const showGutter = attrs.showGutter !== undefined; + const tabSize = attrs.tabSize || DEFAULT_TAB_SIZE; + const behavioursEnabled = attrs.behavioursEnabled ? attrs.behavioursEnabled === 'true' : DEFAULT_BEHAVIOURS; + const snippetsEnabled = attrs.snippetsEnabled ? attrs.snippetsEnabled === 'true' : DEFAULT_SNIPPETS; // Initialize editor - let aceElem = elem.get(0); - let codeEditor = ace.edit(aceElem); - let editorSession = codeEditor.getSession(); + const aceElem = elem.get(0); + const codeEditor = ace.edit(aceElem); + const editorSession = codeEditor.getSession(); - let editorOptions = { + const editorOptions = { maxLines: maxLines, showGutter: showGutter, tabSize: tabSize, @@ -93,7 +93,7 @@ function link(scope, elem, attrs) { // Add classes elem.addClass('gf-code-editor'); - let textarea = elem.find('textarea'); + const textarea = elem.find('textarea'); textarea.addClass('gf-form-input'); if (scope.codeEditorFocus) { @@ -110,14 +110,14 @@ function link(scope, elem, attrs) { // Event handlers editorSession.on('change', e => { scope.$apply(() => { - let newValue = codeEditor.getValue(); + const newValue = codeEditor.getValue(); scope.content = newValue; }); }); // Sync with outer scope - update editor content if model has been changed from outside of directive. scope.$watch('content', (newValue, oldValue) => { - let editorValue = codeEditor.getValue(); + const editorValue = codeEditor.getValue(); if (newValue !== editorValue && newValue !== oldValue) { scope.$$postDigest(function() { setEditorContent(newValue); @@ -157,7 +157,7 @@ function link(scope, elem, attrs) { anyEditor.completers.push(scope.getCompleter()); } - let aceModeName = `ace/mode/${lang}`; + const aceModeName = `ace/mode/${lang}`; editorSession.setMode(aceModeName); } diff --git a/public/app/core/components/colorpicker/ColorPicker.tsx b/public/app/core/components/colorpicker/ColorPicker.tsx index c492d3829ca..6e5083b6d6b 100644 --- a/public/app/core/components/colorpicker/ColorPicker.tsx +++ b/public/app/core/components/colorpicker/ColorPicker.tsx @@ -29,10 +29,10 @@ export class ColorPicker extends React.Component { openColorPicker() { const dropContent = ; - let dropContentElem = document.createElement('div'); + const dropContentElem = document.createElement('div'); ReactDOM.render(dropContent, dropContentElem); - let drop = new Drop({ + const drop = new Drop({ target: this.pickerElem[0], content: dropContentElem, position: 'top center', diff --git a/public/app/core/components/colorpicker/ColorPickerPopover.tsx b/public/app/core/components/colorpicker/ColorPickerPopover.tsx index ac7dd6a2738..c42bcfa1d06 100644 --- a/public/app/core/components/colorpicker/ColorPickerPopover.tsx +++ b/public/app/core/components/colorpicker/ColorPickerPopover.tsx @@ -28,7 +28,7 @@ export class ColorPickerPopover extends React.Component { } setColor(color) { - let newColor = tinycolor(color); + const newColor = tinycolor(color); if (newColor.isValid()) { this.setState({ color: newColor.toString(), @@ -43,20 +43,20 @@ export class ColorPickerPopover extends React.Component { } spectrumColorSelected(color) { - let rgbColor = color.toRgbString(); + const rgbColor = color.toRgbString(); this.setColor(rgbColor); } onColorStringChange(e) { - let colorString = e.target.value; + const colorString = e.target.value; this.setState({ colorString: colorString, }); - let newColor = tinycolor(colorString); + const newColor = tinycolor(colorString); if (newColor.isValid()) { // Update only color state - let newColorString = newColor.toString(); + const newColorString = newColor.toString(); this.setState({ color: newColorString, }); @@ -65,7 +65,7 @@ export class ColorPickerPopover extends React.Component { } onColorStringBlur(e) { - let colorString = e.target.value; + const colorString = e.target.value; this.setColor(colorString); } @@ -73,7 +73,7 @@ export class ColorPickerPopover extends React.Component { this.pickerNavElem.find('li:first').addClass('active'); this.pickerNavElem.on('show', e => { // use href attr (#name => name) - let tab = e.target.hash.slice(1); + const tab = e.target.hash.slice(1); this.setState({ tab: tab, }); diff --git a/public/app/core/components/colorpicker/SpectrumPicker.tsx b/public/app/core/components/colorpicker/SpectrumPicker.tsx index e8a30e8c460..15a76068e9b 100644 --- a/public/app/core/components/colorpicker/SpectrumPicker.tsx +++ b/public/app/core/components/colorpicker/SpectrumPicker.tsx @@ -29,7 +29,7 @@ export class SpectrumPicker extends React.Component { } componentDidMount() { - let spectrumOptions = _.assignIn( + const spectrumOptions = _.assignIn( { flat: true, showAlpha: true, diff --git a/public/app/core/components/form_dropdown/form_dropdown.ts b/public/app/core/components/form_dropdown/form_dropdown.ts index 7ac55e54cf1..007c7c3acb1 100644 --- a/public/app/core/components/form_dropdown/form_dropdown.ts +++ b/public/app/core/components/form_dropdown/form_dropdown.ts @@ -132,7 +132,7 @@ export class FormDropdownCtrl { this.optionCache = options; // extract texts - let optionTexts = _.map(options, op => { + const optionTexts = _.map(options, op => { return _.escape(op.text); }); diff --git a/public/app/core/components/grafana_app.ts b/public/app/core/components/grafana_app.ts index bd6b6975006..1f55bc332ac 100644 --- a/public/app/core/components/grafana_app.ts +++ b/public/app/core/components/grafana_app.ts @@ -140,7 +140,7 @@ export function grafanaAppDirective(playlistSrv, contextSrv, $timeout, $rootScop } // close all drops - for (let drop of Drop.drops) { + for (const drop of Drop.drops) { drop.destroy(); } }); diff --git a/public/app/core/components/info_popover.ts b/public/app/core/components/info_popover.ts index 59332a6f716..ae4feeec701 100644 --- a/public/app/core/components/info_popover.ts +++ b/public/app/core/components/info_popover.ts @@ -8,10 +8,10 @@ export function infoPopover() { template: '', transclude: true, link: function(scope, elem, attrs, ctrl, transclude) { - let offset = attrs.offset || '0 -10px'; - let position = attrs.position || 'right middle'; + const offset = attrs.offset || '0 -10px'; + const position = attrs.position || 'right middle'; let classes = 'drop-help drop-hide-out-of-bounds'; - let openOn = 'hover'; + const openOn = 'hover'; elem.addClass('gf-form-help-icon'); @@ -24,14 +24,14 @@ export function infoPopover() { } transclude(function(clone, newScope) { - let content = document.createElement('div'); + const content = document.createElement('div'); content.className = 'markdown-html'; _.each(clone, node => { content.appendChild(node); }); - let dropOptions = { + const dropOptions = { target: elem[0], content: content, position: position, @@ -52,9 +52,9 @@ export function infoPopover() { // Create drop in next digest after directive content is rendered. scope.$applyAsync(() => { - let drop = new Drop(dropOptions); + const drop = new Drop(dropOptions); - let unbind = scope.$on('$destroy', function() { + const unbind = scope.$on('$destroy', function() { drop.destroy(); unbind(); }); diff --git a/public/app/core/components/manage_dashboards/manage_dashboards.ts b/public/app/core/components/manage_dashboards/manage_dashboards.ts index 86cd3066c48..59a34d08c12 100644 --- a/public/app/core/components/manage_dashboards/manage_dashboards.ts +++ b/public/app/core/components/manage_dashboards/manage_dashboards.ts @@ -103,10 +103,10 @@ export class ManageDashboardsCtrl { this.sections = result; - for (let section of this.sections) { + for (const section of this.sections) { section.checked = false; - for (let dashboard of section.items) { + for (const dashboard of section.items) { dashboard.checked = false; } } @@ -119,7 +119,7 @@ export class ManageDashboardsCtrl { selectionChanged() { let selectedDashboards = 0; - for (let section of this.sections) { + for (const section of this.sections) { selectedDashboards += _.filter(section.items, { checked: true }).length; } @@ -129,7 +129,7 @@ export class ManageDashboardsCtrl { } getFoldersAndDashboardsToDelete() { - let selectedDashboards = { + const selectedDashboards = { folders: [], dashboards: [], }; @@ -148,7 +148,7 @@ export class ManageDashboardsCtrl { getFolderIds(sections) { const ids = []; - for (let s of sections) { + for (const s of sections) { if (s.checked) { ids.push(s.id); } @@ -191,7 +191,7 @@ export class ManageDashboardsCtrl { } getDashboardsToMove() { - let selectedDashboards = []; + const selectedDashboards = []; for (const section of this.sections) { const selected = _.filter(section.items, { checked: true }); @@ -264,7 +264,7 @@ export class ManageDashboardsCtrl { } onSelectAllChanged() { - for (let section of this.sections) { + for (const section of this.sections) { if (!section.hideHeader) { section.checked = this.selectAllChecked; } diff --git a/public/app/core/components/scroll/scroll.ts b/public/app/core/components/scroll/scroll.ts index 3f9865e6dce..5cdbdb62ee3 100644 --- a/public/app/core/components/scroll/scroll.ts +++ b/public/app/core/components/scroll/scroll.ts @@ -17,7 +17,7 @@ export function geminiScrollbar() { restrict: 'A', link: function(scope, elem, attrs) { let scrollRoot = elem.parent(); - let scroller = elem; + const scroller = elem; if (attrs.grafanaScrollbar && attrs.grafanaScrollbar === 'scrollonroot') { scrollRoot = scroller; @@ -27,7 +27,7 @@ export function geminiScrollbar() { $(scrollBarHTML).appendTo(scrollRoot); elem.addClass(scrollerClass); - let scrollParams = { + const scrollParams = { root: scrollRoot[0], scroller: scroller[0], bar: '.baron__bar', @@ -37,7 +37,7 @@ export function geminiScrollbar() { direction: 'v', }; - let scrollbar = baron(scrollParams); + const scrollbar = baron(scrollParams); let lastPos = 0; diff --git a/public/app/core/components/search/SearchResult.tsx b/public/app/core/components/search/SearchResult.tsx index 5ab4bba8edb..3141d29ac7f 100644 --- a/public/app/core/components/search/SearchResult.tsx +++ b/public/app/core/components/search/SearchResult.tsx @@ -54,7 +54,7 @@ export class SearchResultSection extends React.Component { }; render() { - let collapseClassNames = classNames({ + const collapseClassNames = classNames({ fa: true, 'fa-plus': !this.props.section.expanded, 'fa-minus': this.props.section.expanded, diff --git a/public/app/core/components/sidemenu/sidemenu.ts b/public/app/core/components/sidemenu/sidemenu.ts index fb9d9be7f70..5649963c3dc 100644 --- a/public/app/core/components/sidemenu/sidemenu.ts +++ b/public/app/core/components/sidemenu/sidemenu.ts @@ -17,13 +17,13 @@ export class SideMenuCtrl { this.isSignedIn = contextSrv.isSignedIn; this.user = contextSrv.user; - let navTree = _.cloneDeep(config.bootData.navTree); + const navTree = _.cloneDeep(config.bootData.navTree); this.mainLinks = _.filter(navTree, item => !item.hideFromMenu); this.bottomNav = _.filter(navTree, item => item.hideFromMenu); this.loginUrl = 'login?redirect=' + encodeURIComponent(this.$location.path()); if (contextSrv.user.orgCount > 1) { - let profileNode = _.find(this.bottomNav, { id: 'profile' }); + const profileNode = _.find(this.bottomNav, { id: 'profile' }); if (profileNode) { profileNode.showOrgSwitcher = true; } diff --git a/public/app/core/controllers/login_ctrl.ts b/public/app/core/controllers/login_ctrl.ts index 0a66f83d08a..6662686b238 100644 --- a/public/app/core/controllers/login_ctrl.ts +++ b/public/app/core/controllers/login_ctrl.ts @@ -45,8 +45,8 @@ export class LoginCtrl { }; $scope.changeView = function() { - let loginView = document.querySelector('#login-view'); - let changePasswordView = document.querySelector('#change-password-view'); + const loginView = document.querySelector('#login-view'); + const changePasswordView = document.querySelector('#change-password-view'); loginView.className += ' add'; setTimeout(() => { diff --git a/public/app/core/directives/dropdown_typeahead.ts b/public/app/core/directives/dropdown_typeahead.ts index c9e44c5e786..af8c4ddc3bb 100644 --- a/public/app/core/directives/dropdown_typeahead.ts +++ b/public/app/core/directives/dropdown_typeahead.ts @@ -4,12 +4,12 @@ import coreModule from '../core_module'; /** @ngInject */ export function dropdownTypeahead($compile) { - let inputTemplate = + const inputTemplate = ''; - let buttonTemplate = + const buttonTemplate = ''; @@ -21,8 +21,8 @@ export function dropdownTypeahead($compile) { model: '=ngModel', }, link: function($scope, elem, attrs) { - let $input = $(inputTemplate); - let $button = $(buttonTemplate); + const $input = $(inputTemplate); + const $button = $(buttonTemplate); $input.appendTo(elem); $button.appendTo(elem); @@ -42,7 +42,7 @@ export function dropdownTypeahead($compile) { }); } - let typeaheadValues = _.reduce( + const typeaheadValues = _.reduce( $scope.menuItems, function(memo, value, index) { if (!value.submenu) { @@ -60,8 +60,8 @@ export function dropdownTypeahead($compile) { ); $scope.menuItemSelected = function(index, subIndex) { - let menuItem = $scope.menuItems[index]; - let payload: any = { $item: menuItem }; + const menuItem = $scope.menuItems[index]; + const payload: any = { $item: menuItem }; if (menuItem.submenu && subIndex !== void 0) { payload.$subItem = menuItem.submenu[subIndex]; } @@ -74,7 +74,7 @@ export function dropdownTypeahead($compile) { minLength: 1, items: 10, updater: function(value) { - let result: any = {}; + const result: any = {}; _.each($scope.menuItems, function(menuItem) { _.each(menuItem.submenu, function(submenuItem) { if (value === menuItem.text + ' ' + submenuItem.text) { @@ -124,10 +124,10 @@ export function dropdownTypeahead($compile) { /** @ngInject */ export function dropdownTypeahead2($compile) { - let inputTemplate = + const inputTemplate = ''; - let buttonTemplate = + const buttonTemplate = ''; @@ -139,8 +139,8 @@ export function dropdownTypeahead2($compile) { model: '=ngModel', }, link: function($scope, elem, attrs) { - let $input = $(inputTemplate); - let $button = $(buttonTemplate); + const $input = $(inputTemplate); + const $button = $(buttonTemplate); $input.appendTo(elem); $button.appendTo(elem); @@ -160,7 +160,7 @@ export function dropdownTypeahead2($compile) { }); } - let typeaheadValues = _.reduce( + const typeaheadValues = _.reduce( $scope.menuItems, function(memo, value, index) { if (!value.submenu) { @@ -178,8 +178,8 @@ export function dropdownTypeahead2($compile) { ); $scope.menuItemSelected = function(index, subIndex) { - let menuItem = $scope.menuItems[index]; - let payload: any = { $item: menuItem }; + const menuItem = $scope.menuItems[index]; + const payload: any = { $item: menuItem }; if (menuItem.submenu && subIndex !== void 0) { payload.$subItem = menuItem.submenu[subIndex]; } @@ -192,7 +192,7 @@ export function dropdownTypeahead2($compile) { minLength: 1, items: 10, updater: function(value) { - let result: any = {}; + const result: any = {}; _.each($scope.menuItems, function(menuItem) { _.each(menuItem.submenu, function(submenuItem) { if (value === menuItem.text + ' ' + submenuItem.text) { diff --git a/public/app/core/directives/metric_segment.ts b/public/app/core/directives/metric_segment.ts index 3718d7fbd4a..117f776f487 100644 --- a/public/app/core/directives/metric_segment.ts +++ b/public/app/core/directives/metric_segment.ts @@ -4,16 +4,16 @@ import coreModule from '../core_module'; /** @ngInject */ export function metricSegment($compile, $sce) { - let inputTemplate = + const inputTemplate = ''; - let linkTemplate = + const linkTemplate = ''; - let selectTemplate = + const selectTemplate = ''; @@ -25,13 +25,13 @@ export function metricSegment($compile, $sce) { debounce: '@', }, link: function($scope, elem) { - let $input = $(inputTemplate); - let segment = $scope.segment; - let $button = $(segment.selectMode ? selectTemplate : linkTemplate); + const $input = $(inputTemplate); + const segment = $scope.segment; + const $button = $(segment.selectMode ? selectTemplate : linkTemplate); let options = null; let cancelBlur = null; let linkMode = true; - let debounceLookup = $scope.debounce; + const debounceLookup = $scope.debounce; $input.appendTo(elem); $button.appendTo(elem); @@ -44,7 +44,7 @@ export function metricSegment($compile, $sce) { value = _.unescape(value); $scope.$apply(function() { - let selected = _.find($scope.altSegments, { value: value }); + const selected = _.find($scope.altSegments, { value: value }); if (selected) { segment.value = selected.value; segment.html = selected.html || selected.value; @@ -141,10 +141,10 @@ export function metricSegment($compile, $sce) { matcher: $scope.matcher, }); - let typeahead = $input.data('typeahead'); + const typeahead = $input.data('typeahead'); typeahead.lookup = function() { this.query = this.$element.val() || ''; - let items = this.source(this.query, $.proxy(this.process, this)); + const items = this.source(this.query, $.proxy(this.process, this)); return items ? this.process(items) : items; }; @@ -169,7 +169,7 @@ export function metricSegment($compile, $sce) { linkMode = false; - let typeahead = $input.data('typeahead'); + const typeahead = $input.data('typeahead'); if (typeahead) { $input.val(''); typeahead.lookup(); @@ -200,8 +200,8 @@ export function metricSegmentModel(uiSegmentSrv, $q) { let cachedOptions; $scope.valueToSegment = function(value) { - let option = _.find($scope.options, { value: value }); - let segment = { + const option = _.find($scope.options, { value: value }); + const segment = { cssClass: attrs.cssClass, custom: attrs.custom, value: option ? option.text : value, @@ -234,7 +234,7 @@ export function metricSegmentModel(uiSegmentSrv, $q) { $scope.onSegmentChange = function() { if (cachedOptions) { - let option = _.find(cachedOptions, { text: $scope.segment.value }); + const option = _.find(cachedOptions, { text: $scope.segment.value }); if (option && option.value !== $scope.property) { $scope.property = option.value; } else if (attrs.custom !== 'false') { diff --git a/public/app/core/directives/misc.ts b/public/app/core/directives/misc.ts index 299de05f112..034b312aa0e 100644 --- a/public/app/core/directives/misc.ts +++ b/public/app/core/directives/misc.ts @@ -156,7 +156,7 @@ function gfDropdown($parse, $compile, $timeout) { var ul = ['']; for (let index = 0; index < items.length; index++) { - let item = items[index]; + const item = items[index]; if (item.divider) { ul.splice(index + 1, 0, '
  • '); diff --git a/public/app/core/directives/value_select_dropdown.ts b/public/app/core/directives/value_select_dropdown.ts index d384904c2d8..69504c1bb1b 100644 --- a/public/app/core/directives/value_select_dropdown.ts +++ b/public/app/core/directives/value_select_dropdown.ts @@ -46,16 +46,16 @@ export class ValueSelectDropdownCtrl { } updateLinkText() { - let current = this.variable.current; + const current = this.variable.current; if (current.tags && current.tags.length) { // filer out values that are in selected tags - let selectedAndNotInTag = _.filter(this.variable.options, option => { + const selectedAndNotInTag = _.filter(this.variable.options, option => { if (!option.selected) { return false; } for (let i = 0; i < current.tags.length; i++) { - let tag = current.tags[i]; + const tag = current.tags[i]; if (_.indexOf(tag.values, option.value) !== -1) { return false; } @@ -64,7 +64,7 @@ export class ValueSelectDropdownCtrl { }); // convert values to text - let currentTexts = _.map(selectedAndNotInTag, 'text'); + const currentTexts = _.map(selectedAndNotInTag, 'text'); // join texts this.linkText = currentTexts.join(' + '); @@ -142,7 +142,7 @@ export class ValueSelectDropdownCtrl { commitChange = commitChange || false; excludeOthers = excludeOthers || false; - let setAllExceptCurrentTo = newValue => { + const setAllExceptCurrentTo = newValue => { _.each(this.options, other => { if (option !== other) { other.selected = newValue; @@ -246,9 +246,9 @@ export function valueSelectDropdown($compile, $window, $timeout, $rootScope) { controllerAs: 'vm', bindToController: true, link: function(scope, elem) { - let bodyEl = angular.element($window.document.body); - let linkEl = elem.find('.variable-value-link'); - let inputEl = elem.find('input'); + const bodyEl = angular.element($window.document.body); + const linkEl = elem.find('.variable-value-link'); + const inputEl = elem.find('input'); function openDropdown() { inputEl.css('width', Math.max(linkEl.width(), 80) + 'px'); @@ -288,7 +288,7 @@ export function valueSelectDropdown($compile, $window, $timeout, $rootScope) { } }); - let cleanUp = $rootScope.$on('template-variable-value-updated', () => { + const cleanUp = $rootScope.$on('template-variable-value-updated', () => { scope.vm.updateLinkText(); }); diff --git a/public/app/core/nav_model_srv.ts b/public/app/core/nav_model_srv.ts index a9ebd4e79ed..2bed33e70da 100644 --- a/public/app/core/nav_model_srv.ts +++ b/public/app/core/nav_model_srv.ts @@ -41,14 +41,14 @@ export class NavModelSrv { var children = this.navItems; var nav = new NavModel(); - for (let id of args) { + for (const id of args) { // if its a number then it's the index to use for main if (_.isNumber(id)) { nav.main = nav.breadcrumbs[id]; break; } - let node = _.find(children, { id: id }); + const node = _.find(children, { id: id }); nav.breadcrumbs.push(node); nav.node = node; nav.main = node; @@ -56,7 +56,7 @@ export class NavModelSrv { } if (nav.main.children) { - for (let item of nav.main.children) { + for (const item of nav.main.children) { item.active = false; if (item.url === nav.node.url) { diff --git a/public/app/core/services/backend_srv.ts b/public/app/core/services/backend_srv.ts index 1aeeedef4dd..4dd8a123378 100644 --- a/public/app/core/services/backend_srv.ts +++ b/public/app/core/services/backend_srv.ts @@ -276,11 +276,11 @@ export class BackendSrv { deleteFoldersAndDashboards(folderUids, dashboardUids) { const tasks = []; - for (let folderUid of folderUids) { + for (const folderUid of folderUids) { tasks.push(this.createTask(this.deleteFolder.bind(this), true, folderUid, true)); } - for (let dashboardUid of dashboardUids) { + for (const dashboardUid of dashboardUids) { tasks.push(this.createTask(this.deleteDashboard.bind(this), true, dashboardUid, true)); } @@ -290,7 +290,7 @@ export class BackendSrv { moveDashboards(dashboardUids, toFolder) { const tasks = []; - for (let uid of dashboardUids) { + for (const uid of dashboardUids) { tasks.push(this.createTask(this.moveDashboard.bind(this), true, uid, toFolder)); } @@ -304,7 +304,7 @@ export class BackendSrv { } private moveDashboard(uid, toFolder) { - let deferred = this.$q.defer(); + const deferred = this.$q.defer(); this.getDashboardByUid(uid).then(fullDash => { const model = new DashboardModel(fullDash.dashboard, fullDash.meta); @@ -315,7 +315,7 @@ export class BackendSrv { } const clone = model.getSaveModelClone(); - let options = { + const options = { folderId: toFolder.id, overwrite: false, }; diff --git a/public/app/core/services/bridge_srv.ts b/public/app/core/services/bridge_srv.ts index 4a5649a6c52..bdc2976a94c 100644 --- a/public/app/core/services/bridge_srv.ts +++ b/public/app/core/services/bridge_srv.ts @@ -15,7 +15,7 @@ export class BridgeSrv { init() { this.$rootScope.$on('$routeUpdate', (evt, data) => { - let angularUrl = this.$location.url(); + const angularUrl = this.$location.url(); if (store.view.currentUrl !== angularUrl) { store.view.updatePathAndQuery(this.$location.path(), this.$location.search(), this.$route.current.params); } @@ -28,7 +28,7 @@ export class BridgeSrv { reaction( () => store.view.currentUrl, currentUrl => { - let angularUrl = this.$location.url(); + const angularUrl = this.$location.url(); const url = locationUtil.stripBaseFromUrl(currentUrl); if (angularUrl !== url) { this.$timeout(() => { diff --git a/public/app/core/services/dynamic_directive_srv.ts b/public/app/core/services/dynamic_directive_srv.ts index 086843b6f9a..de06daf2c1a 100644 --- a/public/app/core/services/dynamic_directive_srv.ts +++ b/public/app/core/services/dynamic_directive_srv.ts @@ -36,7 +36,7 @@ class DynamicDirectiveSrv { } create(options) { - let directiveDef = { + const directiveDef = { restrict: 'E', scope: options.scope, link: (scope, elem, attrs) => { diff --git a/public/app/core/services/keybindingSrv.ts b/public/app/core/services/keybindingSrv.ts index 9d914a94a1c..5405a347ba0 100644 --- a/public/app/core/services/keybindingSrv.ts +++ b/public/app/core/services/keybindingSrv.ts @@ -210,7 +210,7 @@ export class KeybindingSrv { // duplicate panel this.bind('p d', () => { if (dashboard.meta.focusPanelId && dashboard.meta.canEdit) { - let panelIndex = dashboard.getPanelInfoById(dashboard.meta.focusPanelId).index; + const panelIndex = dashboard.getPanelInfoById(dashboard.meta.focusPanelId).index; dashboard.duplicatePanel(dashboard.panels[panelIndex]); } }); diff --git a/public/app/core/services/ng_react.ts b/public/app/core/services/ng_react.ts index 3c61412669e..aeffaaa9b3b 100644 --- a/public/app/core/services/ng_react.ts +++ b/public/app/core/services/ng_react.ts @@ -295,6 +295,6 @@ var reactDirective = function($injector) { }; }; -let ngModule = angular.module('react', []); +const ngModule = angular.module('react', []); ngModule.directive('reactComponent', ['$injector', reactComponent]); ngModule.factory('reactDirective', ['$injector', reactDirective]); diff --git a/public/app/core/services/search_srv.ts b/public/app/core/services/search_srv.ts index 9f32e21f3f6..017b2c15efc 100644 --- a/public/app/core/services/search_srv.ts +++ b/public/app/core/services/search_srv.ts @@ -85,10 +85,10 @@ export class SearchSrv { } search(options) { - let sections: any = {}; - let promises = []; - let query = _.clone(options); - let hasFilters = + const sections: any = {}; + const promises = []; + const query = _.clone(options); + const hasFilters = options.query || (options.tag && options.tag.length > 0) || options.starred || @@ -124,7 +124,7 @@ export class SearchSrv { } // create folder index - for (let hit of results) { + for (const hit of results) { if (hit.type === 'dash-folder') { sections[hit.id] = { id: hit.id, @@ -140,7 +140,7 @@ export class SearchSrv { } } - for (let hit of results) { + for (const hit of results) { if (hit.type === 'dash-folder') { continue; } @@ -185,7 +185,7 @@ export class SearchSrv { return Promise.resolve(section); } - let query = { + const query = { folderIds: [section.id], }; diff --git a/public/app/core/services/segment_srv.ts b/public/app/core/services/segment_srv.ts index 042340e6102..5250febc11a 100644 --- a/public/app/core/services/segment_srv.ts +++ b/public/app/core/services/segment_srv.ts @@ -3,7 +3,7 @@ import coreModule from '../core_module'; /** @ngInject */ export function uiSegmentSrv($sce, templateSrv) { - let self = this; + const self = this; function MetricSegment(options) { if (options === '*' || options.value === '*') { @@ -78,7 +78,7 @@ export function uiSegmentSrv($sce, templateSrv) { this.transformToSegments = function(addTemplateVars, variableTypeFilter) { return function(results) { - let segments = _.map(results, function(segment) { + const segments = _.map(results, function(segment) { return self.newSegment({ value: segment.text, expandable: segment.expandable }); }); diff --git a/public/app/core/specs/backend_srv.test.ts b/public/app/core/specs/backend_srv.test.ts index b19bd117766..e9cd5973d36 100644 --- a/public/app/core/specs/backend_srv.test.ts +++ b/public/app/core/specs/backend_srv.test.ts @@ -2,14 +2,14 @@ import { BackendSrv } from 'app/core/services/backend_srv'; jest.mock('app/core/store'); describe('backend_srv', function() { - let _httpBackend = options => { + const _httpBackend = options => { if (options.url === 'gateway-error') { return Promise.reject({ status: 502 }); } return Promise.resolve({}); }; - let _backendSrv = new BackendSrv(_httpBackend, {}, {}, {}, {}); + const _backendSrv = new BackendSrv(_httpBackend, {}, {}, {}, {}); describe('when handling errors', () => { it('should return the http status code', async () => { diff --git a/public/app/core/specs/file_export.test.ts b/public/app/core/specs/file_export.test.ts index 915ce08fcd2..ced94fcdbc0 100644 --- a/public/app/core/specs/file_export.test.ts +++ b/public/app/core/specs/file_export.test.ts @@ -2,7 +2,7 @@ import * as fileExport from '../utils/file_export'; import { beforeEach, expect } from 'test/lib/common'; describe('file_export', () => { - let ctx: any = {}; + const ctx: any = {}; beforeEach(() => { ctx.seriesList = [ @@ -28,7 +28,7 @@ describe('file_export', () => { describe('when exporting series as rows', () => { it('should export points in proper order', () => { - let text = fileExport.convertSeriesListToCsv(ctx.seriesList, ctx.timeFormat); + const text = fileExport.convertSeriesListToCsv(ctx.seriesList, ctx.timeFormat); const expectedText = '"Series";"Time";"Value"\r\n' + '"series_1";"1500026100";1\r\n' + @@ -48,7 +48,7 @@ describe('file_export', () => { describe('when exporting series as columns', () => { it('should export points in proper order', () => { - let text = fileExport.convertSeriesListToCsvColumns(ctx.seriesList, ctx.timeFormat); + const text = fileExport.convertSeriesListToCsvColumns(ctx.seriesList, ctx.timeFormat); const expectedText = '"Time";"series_1";"series_2"\r\n' + '"1500026100";1;11\r\n' + diff --git a/public/app/core/specs/search.test.ts b/public/app/core/specs/search.test.ts index 8aea35af213..3cc789b3cc5 100644 --- a/public/app/core/specs/search.test.ts +++ b/public/app/core/specs/search.test.ts @@ -12,7 +12,7 @@ describe('SearchCtrl', () => { search: (options: any) => {}, getDashboardTags: () => {}, }; - let ctrl = new SearchCtrl({ $on: () => {} }, {}, {}, searchSrvStub); + const ctrl = new SearchCtrl({ $on: () => {} }, {}, {}, searchSrvStub); describe('Given an empty result', () => { beforeEach(() => { diff --git a/public/app/core/specs/search_results.test.ts b/public/app/core/specs/search_results.test.ts index 830496be3a8..96dbc8bb963 100644 --- a/public/app/core/specs/search_results.test.ts +++ b/public/app/core/specs/search_results.test.ts @@ -12,7 +12,7 @@ describe('SearchResultsCtrl', () => { let ctrl; describe('when checking an item that is not checked', () => { - let item = { checked: false }; + const item = { checked: false }; let selectionChanged = false; beforeEach(() => { @@ -31,7 +31,7 @@ describe('SearchResultsCtrl', () => { }); describe('when checking an item that is checked', () => { - let item = { checked: true }; + const item = { checked: true }; let selectionChanged = false; beforeEach(() => { @@ -72,7 +72,7 @@ describe('SearchResultsCtrl', () => { folderExpanded = true; }; - let folder = { + const folder = { expanded: false, toggle: () => Promise.resolve(folder), }; @@ -94,7 +94,7 @@ describe('SearchResultsCtrl', () => { folderExpanded = true; }; - let folder = { + const folder = { expanded: true, toggle: () => Promise.resolve(folder), }; diff --git a/public/app/core/specs/ticks.test.ts b/public/app/core/specs/ticks.test.ts index 8b7e0cd73b5..73d0e96cbd2 100644 --- a/public/app/core/specs/ticks.test.ts +++ b/public/app/core/specs/ticks.test.ts @@ -2,7 +2,7 @@ import * as ticks from '../utils/ticks'; describe('ticks', () => { describe('getFlotTickDecimals()', () => { - let ctx: any = {}; + const ctx: any = {}; beforeEach(() => { ctx.axis = {}; diff --git a/public/app/core/specs/time_series.test.ts b/public/app/core/specs/time_series.test.ts index bf50d807e03..35b75b3da5e 100644 --- a/public/app/core/specs/time_series.test.ts +++ b/public/app/core/specs/time_series.test.ts @@ -329,7 +329,7 @@ describe('TimeSeries', function() { describe('legend decimals', function() { let series, panel; - let height = 200; + const height = 200; beforeEach(function() { testData = { alias: 'test', @@ -348,7 +348,7 @@ describe('TimeSeries', function() { }); it('should set decimals based on Y axis (expect calculated decimals = 1)', function() { - let data = [series]; + const data = [series]; // Expect ticks with this data will have decimals = 1 updateLegendValues(data, panel, height); expect(data[0].decimals).toBe(2); @@ -358,21 +358,21 @@ describe('TimeSeries', function() { testData.datapoints = [[10, 2], [0, 3], [100, 4], [80, 5]]; series = new TimeSeries(testData); series.getFlotPairs(); - let data = [series]; + const data = [series]; updateLegendValues(data, panel, height); expect(data[0].decimals).toBe(0); }); it('should set decimals to Y axis decimals + 1', function() { panel.yaxes[0].decimals = 2; - let data = [series]; + const data = [series]; updateLegendValues(data, panel, height); expect(data[0].decimals).toBe(3); }); it('should set decimals to legend decimals value if it was set explicitly', function() { panel.decimals = 3; - let data = [series]; + const data = [series]; updateLegendValues(data, panel, height); expect(data[0].decimals).toBe(3); }); diff --git a/public/app/core/specs/value_select_dropdown.test.ts b/public/app/core/specs/value_select_dropdown.test.ts index 3cc310435b7..024774250b8 100644 --- a/public/app/core/specs/value_select_dropdown.test.ts +++ b/public/app/core/specs/value_select_dropdown.test.ts @@ -3,7 +3,7 @@ import { ValueSelectDropdownCtrl } from '../directives/value_select_dropdown'; import q from 'q'; describe('SelectDropdownCtrl', () => { - let tagValuesMap: any = {}; + const tagValuesMap: any = {}; ValueSelectDropdownCtrl.prototype.onUpdated = jest.fn(); let ctrl; diff --git a/public/app/core/time_series2.ts b/public/app/core/time_series2.ts index f4d0943d52f..c29242c9aca 100644 --- a/public/app/core/time_series2.ts +++ b/public/app/core/time_series2.ts @@ -27,11 +27,11 @@ function translateFillOption(fill) { */ export function updateLegendValues(data: TimeSeries[], panel, height) { for (let i = 0; i < data.length; i++) { - let series = data[i]; + const series = data[i]; const yaxes = panel.yaxes; const seriesYAxis = series.yaxis || 1; const axis = yaxes[seriesYAxis - 1]; - let formater = kbn.valueFormats[axis.format]; + const formater = kbn.valueFormats[axis.format]; // decimal override if (_.isNumber(panel.decimals)) { @@ -54,7 +54,7 @@ export function getDataMinMax(data: TimeSeries[]) { let datamin = null; let datamax = null; - for (let series of data) { + for (const series of data) { if (datamax === null || datamax < series.stats.max) { datamax = series.stats.max; } @@ -225,7 +225,7 @@ export default class TimeSeries { // Due to missing values we could have different timeStep all along the series // so we have to find the minimum one (could occur with aggregators such as ZimSum) if (previousTime !== undefined) { - let timeStep = currentTime - previousTime; + const timeStep = currentTime - previousTime; if (timeStep < this.stats.timeStep) { this.stats.timeStep = timeStep; } diff --git a/public/app/core/utils/colors.ts b/public/app/core/utils/colors.ts index 8a70e093ea2..e8a7366beb5 100644 --- a/public/app/core/utils/colors.ts +++ b/public/app/core/utils/colors.ts @@ -9,7 +9,7 @@ export const ALERTING_COLOR = 'rgba(237, 46, 24, 1)'; export const NO_DATA_COLOR = 'rgba(150, 150, 150, 1)'; export const REGION_FILL_ALPHA = 0.09; -let colors = [ +const colors = [ '#7EB26D', '#EAB839', '#6ED0E0', @@ -69,7 +69,7 @@ let colors = [ ]; export function sortColorsByHue(hexColors) { - let hslColors = _.map(hexColors, hexToHsl); + const hslColors = _.map(hexColors, hexToHsl); let sortedHSLColors = _.sortBy(hslColors, ['h']); sortedHSLColors = _.chunk(sortedHSLColors, PALETTE_ROWS); diff --git a/public/app/core/utils/dag.test.ts b/public/app/core/utils/dag.test.ts index a89ab27cda3..064da13806b 100644 --- a/public/app/core/utils/dag.test.ts +++ b/public/app/core/utils/dag.test.ts @@ -2,16 +2,16 @@ import { Graph } from './dag'; describe('Directed acyclic graph', () => { describe('Given a graph with nodes with different links in between them', () => { - let dag = new Graph(); - let nodeA = dag.createNode('A'); - let nodeB = dag.createNode('B'); - let nodeC = dag.createNode('C'); - let nodeD = dag.createNode('D'); - let nodeE = dag.createNode('E'); - let nodeF = dag.createNode('F'); - let nodeG = dag.createNode('G'); - let nodeH = dag.createNode('H'); - let nodeI = dag.createNode('I'); + const dag = new Graph(); + const nodeA = dag.createNode('A'); + const nodeB = dag.createNode('B'); + const nodeC = dag.createNode('C'); + const nodeD = dag.createNode('D'); + const nodeE = dag.createNode('E'); + const nodeF = dag.createNode('F'); + const nodeG = dag.createNode('G'); + const nodeH = dag.createNode('H'); + const nodeI = dag.createNode('I'); dag.link([nodeB, nodeC, nodeD, nodeE, nodeF, nodeG, nodeH], nodeA); dag.link([nodeC, nodeD, nodeE, nodeF, nodeI], nodeB); dag.link([nodeD, nodeE, nodeF, nodeG], nodeC); diff --git a/public/app/core/utils/dag.ts b/public/app/core/utils/dag.ts index 1d61280fb05..eb7ff1c3b1a 100644 --- a/public/app/core/utils/dag.ts +++ b/public/app/core/utils/dag.ts @@ -26,8 +26,8 @@ export class Edge { unlink() { let pos; - let inode = this.inputNode; - let onode = this.outputNode; + const inode = this.inputNode; + const onode = this.outputNode; if (!(inode && onode)) { return; @@ -96,12 +96,12 @@ export class Node { } getOptimizedInputEdges(): Edge[] { - let toBeRemoved = []; + const toBeRemoved = []; this.inputEdges.forEach(e => { - let inputEdgesNodes = e.inputNode.inputEdges.map(e => e.inputNode); + const inputEdgesNodes = e.inputNode.inputEdges.map(e => e.inputNode); inputEdgesNodes.forEach(n => { - let edgeToRemove = n.getEdgeTo(this.name); + const edgeToRemove = n.getEdgeTo(this.name); if (edgeToRemove) { toBeRemoved.push(edgeToRemove); } @@ -124,7 +124,7 @@ export class Graph { } createNodes(names: string[]): Node[] { - let nodes = []; + const nodes = []; names.forEach(name => { nodes.push(this.createNode(name)); }); @@ -134,8 +134,8 @@ export class Graph { link(input: string | string[] | Node | Node[], output: string | string[] | Node | Node[]): Edge[] { let inputArr = []; let outputArr = []; - let inputNodes = []; - let outputNodes = []; + const inputNodes = []; + const outputNodes = []; if (input instanceof Array) { inputArr = input; @@ -167,7 +167,7 @@ export class Graph { } } - let edges = []; + const edges = []; inputNodes.forEach(input => { outputNodes.forEach(output => { edges.push(this.createEdge().link(input, output)); diff --git a/public/app/core/utils/file_export.ts b/public/app/core/utils/file_export.ts index f25d340a0be..298a06c64fd 100644 --- a/public/app/core/utils/file_export.ts +++ b/public/app/core/utils/file_export.ts @@ -74,7 +74,7 @@ export function convertSeriesListToCsv(seriesList, dateTimeFormat = DEFAULT_DATE } export function exportSeriesListToCsv(seriesList, dateTimeFormat = DEFAULT_DATETIME_FORMAT, excel = false) { - let text = convertSeriesListToCsv(seriesList, dateTimeFormat, excel); + const text = convertSeriesListToCsv(seriesList, dateTimeFormat, excel); saveSaveBlob(text, EXPORT_FILENAME); } @@ -115,7 +115,7 @@ export function convertSeriesListToCsvColumns(seriesList, dateTimeFormat = DEFAU function mergeSeriesByTime(seriesList) { let timestamps = []; for (let i = 0; i < seriesList.length; i++) { - let seriesPoints = seriesList[i].datapoints; + const seriesPoints = seriesList[i].datapoints; for (let j = 0; j < seriesPoints.length; j++) { timestamps.push(seriesPoints[j][POINT_TIME_INDEX]); } @@ -123,9 +123,9 @@ function mergeSeriesByTime(seriesList) { timestamps = sortedUniq(timestamps.sort()); for (let i = 0; i < seriesList.length; i++) { - let seriesPoints = seriesList[i].datapoints; - let seriesTimestamps = seriesPoints.map(p => p[POINT_TIME_INDEX]); - let extendedSeries = []; + const seriesPoints = seriesList[i].datapoints; + const seriesTimestamps = seriesPoints.map(p => p[POINT_TIME_INDEX]); + const extendedSeries = []; let pointIndex; for (let j = 0; j < timestamps.length; j++) { pointIndex = sortedIndexOf(seriesTimestamps, timestamps[j]); @@ -141,7 +141,7 @@ function mergeSeriesByTime(seriesList) { } export function exportSeriesListToCsvColumns(seriesList, dateTimeFormat = DEFAULT_DATETIME_FORMAT, excel = false) { - let text = convertSeriesListToCsvColumns(seriesList, dateTimeFormat, excel); + const text = convertSeriesListToCsvColumns(seriesList, dateTimeFormat, excel); saveSaveBlob(text, EXPORT_FILENAME); } @@ -157,11 +157,11 @@ export function convertTableDataToCsv(table, excel = false) { } export function exportTableDataToCsv(table, excel = false) { - let text = convertTableDataToCsv(table, excel); + const text = convertTableDataToCsv(table, excel); saveSaveBlob(text, EXPORT_FILENAME); } export function saveSaveBlob(payload, fname) { - let blob = new Blob([payload], { type: 'text/csv;charset=utf-8;header=present;' }); + const blob = new Blob([payload], { type: 'text/csv;charset=utf-8;header=present;' }); saveAs(blob, fname); } diff --git a/public/app/core/utils/outline.ts b/public/app/core/utils/outline.ts index 94393e781e9..cc06102bfdc 100644 --- a/public/app/core/utils/outline.ts +++ b/public/app/core/utils/outline.ts @@ -1,6 +1,6 @@ // based on http://www.paciellogroup.com/blog/2012/04/how-to-remove-css-outlines-in-an-accessible-manner/ function outlineFixer() { - let d: any = document; + const d: any = document; var style_element = d.createElement('STYLE'); var dom_events = 'addEventListener' in d; diff --git a/public/app/core/utils/rangeutil.ts b/public/app/core/utils/rangeutil.ts index 95cfe42f0b8..8e0f87df686 100644 --- a/public/app/core/utils/rangeutil.ts +++ b/public/app/core/utils/rangeutil.ts @@ -92,7 +92,7 @@ function formatDate(date) { // now/d // if no to then to now is assumed export function describeTextRange(expr: any) { - let isLast = expr.indexOf('+') !== 0; + const isLast = expr.indexOf('+') !== 0; if (expr.indexOf('now') === -1) { expr = (isLast ? 'now-' : 'now') + expr; } @@ -108,11 +108,11 @@ export function describeTextRange(expr: any) { opt = { from: 'now', to: expr }; } - let parts = /^now([-+])(\d+)(\w)/.exec(expr); + const parts = /^now([-+])(\d+)(\w)/.exec(expr); if (parts) { - let unit = parts[3]; - let amount = parseInt(parts[2]); - let span = spans[unit]; + const unit = parts[3]; + const amount = parseInt(parts[2]); + const span = spans[unit]; if (span) { opt.display = isLast ? 'Last ' : 'Next '; opt.display += amount + ' ' + span.display; diff --git a/public/app/core/utils/sort_by_keys.ts b/public/app/core/utils/sort_by_keys.ts index 9dff252576a..0020d04f290 100644 --- a/public/app/core/utils/sort_by_keys.ts +++ b/public/app/core/utils/sort_by_keys.ts @@ -7,7 +7,7 @@ export default function sortByKeys(input) { if (_.isPlainObject(input)) { var sortedObject = {}; - for (let key of _.keys(input).sort()) { + for (const key of _.keys(input).sort()) { sortedObject[key] = sortByKeys(input[key]); } return sortedObject; diff --git a/public/app/core/utils/tags.ts b/public/app/core/utils/tags.ts index 678fd8c94be..d0f244be76b 100644 --- a/public/app/core/utils/tags.ts +++ b/public/app/core/utils/tags.ts @@ -67,9 +67,9 @@ const TAG_BORDER_COLORS = [ * @param name tag name */ export function getTagColorsFromName(name: string): { color: string; borderColor: string } { - let hash = djb2(name.toLowerCase()); - let color = TAG_COLORS[Math.abs(hash % TAG_COLORS.length)]; - let borderColor = TAG_BORDER_COLORS[Math.abs(hash % TAG_BORDER_COLORS.length)]; + const hash = djb2(name.toLowerCase()); + const color = TAG_COLORS[Math.abs(hash % TAG_COLORS.length)]; + const borderColor = TAG_BORDER_COLORS[Math.abs(hash % TAG_BORDER_COLORS.length)]; return { color, borderColor }; } diff --git a/public/app/core/utils/ticks.ts b/public/app/core/utils/ticks.ts index 66e6a7ce4fc..d87dedccab1 100644 --- a/public/app/core/utils/ticks.ts +++ b/public/app/core/utils/ticks.ts @@ -7,7 +7,7 @@ * @param count Ticks count */ export function tickStep(start: number, stop: number, count: number): number { - let e10 = Math.sqrt(50), + const e10 = Math.sqrt(50), e5 = Math.sqrt(10), e2 = Math.sqrt(2); @@ -76,7 +76,7 @@ export function getFlotRange(panelMin, panelMax, datamin, datamax) { let min = +(panelMin != null ? panelMin : datamin); let max = +(panelMax != null ? panelMax : datamax); - let delta = max - min; + const delta = max - min; if (delta === 0.0) { // Grafana fix: wide Y min and max using increased wideFactor @@ -123,11 +123,11 @@ export function getFlotTickDecimals(datamin, datamax, axis, height) { const { min, max } = getFlotRange(axis.min, axis.max, datamin, datamax); const noTicks = 0.3 * Math.sqrt(height); const delta = (max - min) / noTicks; - let dec = -Math.floor(Math.log(delta) / Math.LN10); + const dec = -Math.floor(Math.log(delta) / Math.LN10); - let magn = Math.pow(10, -dec); + const magn = Math.pow(10, -dec); // norm is between 1.0 and 10.0 - let norm = delta / magn; + const norm = delta / magn; let size; if (norm < 1.5) { @@ -159,10 +159,10 @@ export function getFlotTickDecimals(datamin, datamax, axis, height) { */ export function grafanaTimeFormat(ticks, min, max) { if (min && max && ticks) { - let range = max - min; - let secPerTick = range / ticks / 1000; - let oneDay = 86400000; - let oneYear = 31536000000; + const range = max - min; + const secPerTick = range / ticks / 1000; + const oneDay = 86400000; + const oneYear = 31536000000; if (secPerTick <= 45) { return '%H:%M:%S'; @@ -193,7 +193,7 @@ export function logp(value, base) { * Get decimal precision of number (3.14 => 2) */ export function getPrecision(num: number): number { - let str = num.toString(); + const str = num.toString(); return getStringPrecision(str); } @@ -201,7 +201,7 @@ export function getPrecision(num: number): number { * Get decimal precision of number stored as a string ("3.14" => 2) */ export function getStringPrecision(num: string): number { - let dot_index = num.indexOf('.'); + const dot_index = num.indexOf('.'); if (dot_index === -1) { return 0; } else { diff --git a/public/app/core/utils/url.ts b/public/app/core/utils/url.ts index b57d5721d57..857e76d9094 100644 --- a/public/app/core/utils/url.ts +++ b/public/app/core/utils/url.ts @@ -3,14 +3,14 @@ */ export function toUrlParams(a) { - let s = []; - let rbracket = /\[\]$/; + const s = []; + const rbracket = /\[\]$/; - let isArray = function(obj) { + const isArray = function(obj) { return Object.prototype.toString.call(obj) === '[object Array]'; }; - let add = function(k, v) { + const add = function(k, v) { v = typeof v === 'function' ? v() : v === null ? '' : v === undefined ? '' : v; if (typeof v !== 'boolean') { s[s.length] = encodeURIComponent(k) + '=' + encodeURIComponent(v); @@ -19,7 +19,7 @@ export function toUrlParams(a) { } }; - let buildParams = function(prefix, obj) { + const buildParams = function(prefix, obj) { var i, len, key; if (prefix) { diff --git a/public/app/core/utils/version.ts b/public/app/core/utils/version.ts index 6ee1400df51..8b249563d86 100644 --- a/public/app/core/utils/version.ts +++ b/public/app/core/utils/version.ts @@ -9,7 +9,7 @@ export class SemVersion { meta: string; constructor(version: string) { - let match = versionPattern.exec(version); + const match = versionPattern.exec(version); if (match) { this.major = Number(match[1]); this.minor = Number(match[2] || 0); @@ -19,7 +19,7 @@ export class SemVersion { } isGtOrEq(version: string): boolean { - let compared = new SemVersion(version); + const compared = new SemVersion(version); return !(this.major < compared.major || this.minor < compared.minor || this.patch < compared.patch); } @@ -29,6 +29,6 @@ export class SemVersion { } export function isVersionGtOrEq(a: string, b: string): boolean { - let a_semver = new SemVersion(a); + const a_semver = new SemVersion(a); return a_semver.isGtOrEq(b); } diff --git a/public/app/features/alerting/alert_tab_ctrl.ts b/public/app/features/alerting/alert_tab_ctrl.ts index 79baa1e3f5a..a25d37913d4 100644 --- a/public/app/features/alerting/alert_tab_ctrl.ts +++ b/public/app/features/alerting/alert_tab_ctrl.ts @@ -184,7 +184,7 @@ export class AlertTabCtrl { ThresholdMapper.alertToGraphThresholds(this.panel); - for (let addedNotification of alert.notifications) { + for (const addedNotification of alert.notifications) { var model = _.find(this.notifications, { id: addedNotification.id }); if (model && model.isDefault === false) { model.iconClass = this.getNotificationIcon(model.type); @@ -192,7 +192,7 @@ export class AlertTabCtrl { } } - for (let notification of this.notifications) { + for (const notification of this.notifications) { if (notification.isDefault) { notification.iconClass = this.getNotificationIcon(notification.type); notification.bgColor = '#00678b'; diff --git a/public/app/features/alerting/notification_edit_ctrl.ts b/public/app/features/alerting/notification_edit_ctrl.ts index 18b1c4d1d55..eb14766d1fb 100644 --- a/public/app/features/alerting/notification_edit_ctrl.ts +++ b/public/app/features/alerting/notification_edit_ctrl.ts @@ -30,7 +30,7 @@ export class AlertNotificationEditCtrl { this.notifiers = notifiers; // add option templates - for (let notifier of this.notifiers) { + for (const notifier of this.notifiers) { this.$templateCache.put(this.getNotifierTemplateId(notifier.type), notifier.optionsTemplate); } diff --git a/public/app/features/alerting/threshold_mapper.ts b/public/app/features/alerting/threshold_mapper.ts index 9142c74b6e3..50324dc18ca 100644 --- a/public/app/features/alerting/threshold_mapper.ts +++ b/public/app/features/alerting/threshold_mapper.ts @@ -1,7 +1,7 @@ export class ThresholdMapper { static alertToGraphThresholds(panel) { for (var i = 0; i < panel.alert.conditions.length; i++) { - let condition = panel.alert.conditions[i]; + const condition = panel.alert.conditions[i]; if (condition.type !== 'query') { continue; } @@ -11,18 +11,18 @@ export class ThresholdMapper { switch (evaluator.type) { case 'gt': { - let value = evaluator.params[0]; + const value = evaluator.params[0]; thresholds.push({ value: value, op: 'gt' }); break; } case 'lt': { - let value = evaluator.params[0]; + const value = evaluator.params[0]; thresholds.push({ value: value, op: 'lt' }); break; } case 'outside_range': { - let value1 = evaluator.params[0]; - let value2 = evaluator.params[1]; + const value1 = evaluator.params[0]; + const value2 = evaluator.params[1]; if (value1 > value2) { thresholds.push({ value: value1, op: 'gt' }); @@ -35,8 +35,8 @@ export class ThresholdMapper { break; } case 'within_range': { - let value1 = evaluator.params[0]; - let value2 = evaluator.params[1]; + const value1 = evaluator.params[0]; + const value2 = evaluator.params[1]; if (value1 > value2) { thresholds.push({ value: value1, op: 'lt' }); diff --git a/public/app/features/annotations/annotations_srv.ts b/public/app/features/annotations/annotations_srv.ts index 5578a979146..b8def36829a 100644 --- a/public/app/features/annotations/annotations_srv.ts +++ b/public/app/features/annotations/annotations_srv.ts @@ -91,7 +91,7 @@ export class AnnotationsSrv { var range = this.timeSrv.timeRange(); var promises = []; - for (let annotation of dashboard.annotations.list) { + for (const annotation of dashboard.annotations.list) { if (!annotation.enable) { continue; } diff --git a/public/app/features/annotations/event_editor.ts b/public/app/features/annotations/event_editor.ts index 1f94e978029..90c425438ab 100644 --- a/public/app/features/annotations/event_editor.ts +++ b/public/app/features/annotations/event_editor.ts @@ -31,7 +31,7 @@ export class EventEditorCtrl { return; } - let saveModel = _.cloneDeep(this.event); + const saveModel = _.cloneDeep(this.event); saveModel.time = saveModel.time.valueOf(); saveModel.timeEnd = 0; @@ -85,7 +85,7 @@ export class EventEditorCtrl { function tryEpochToMoment(timestamp) { if (timestamp && _.isNumber(timestamp)) { - let epoch = Number(timestamp); + const epoch = Number(timestamp); return moment(epoch); } else { return timestamp; diff --git a/public/app/features/annotations/event_manager.ts b/public/app/features/annotations/event_manager.ts index 7db6a19f2c6..a6fceac2e54 100644 --- a/public/app/features/annotations/event_manager.ts +++ b/public/app/features/annotations/event_manager.ts @@ -125,11 +125,11 @@ export class EventManager { } } - let regions = getRegions(annotations); + const regions = getRegions(annotations); addRegionMarking(regions, flotOptions); - let eventSectionHeight = 20; - let eventSectionMargin = 7; + const eventSectionHeight = 20; + const eventSectionMargin = 7; flotOptions.grid.eventSectionHeight = eventSectionMargin; flotOptions.xaxis.eventSectionHeight = eventSectionHeight; @@ -147,8 +147,8 @@ function getRegions(events) { } function addRegionMarking(regions, flotOptions) { - let markings = flotOptions.grid.markings; - let defaultColor = DEFAULT_ANNOTATION_COLOR; + const markings = flotOptions.grid.markings; + const defaultColor = DEFAULT_ANNOTATION_COLOR; let fillColor; _.each(regions, region => { @@ -167,7 +167,7 @@ function addRegionMarking(regions, flotOptions) { } function addAlphaToRGB(colorString: string, alpha: number): string { - let color = tinycolor(colorString); + const color = tinycolor(colorString); if (color.isValid()) { color.setAlpha(alpha); return color.toRgbString(); diff --git a/public/app/features/annotations/events_processing.ts b/public/app/features/annotations/events_processing.ts index 667285d7d43..6e610fb1457 100644 --- a/public/app/features/annotations/events_processing.ts +++ b/public/app/features/annotations/events_processing.ts @@ -7,20 +7,20 @@ import _ from 'lodash'; * @param options */ export function makeRegions(annotations, options) { - let [regionEvents, singleEvents] = _.partition(annotations, 'regionId'); - let regions = getRegions(regionEvents, options.range); + const [regionEvents, singleEvents] = _.partition(annotations, 'regionId'); + const regions = getRegions(regionEvents, options.range); annotations = _.concat(regions, singleEvents); return annotations; } function getRegions(events, range) { - let region_events = _.filter(events, event => { + const region_events = _.filter(events, event => { return event.regionId; }); let regions = _.groupBy(region_events, 'regionId'); regions = _.compact( _.map(regions, region_events => { - let region_obj = _.head(region_events); + const region_obj = _.head(region_events); if (region_events && region_events.length > 1) { region_obj.timeEnd = region_events[1].time; region_obj.isRegion = true; @@ -57,9 +57,9 @@ export function dedupAnnotations(annotations) { let dedup = []; // Split events by annotationId property existence - let events = _.partition(annotations, 'id'); + const events = _.partition(annotations, 'id'); - let eventsById = _.groupBy(events[0], 'id'); + const eventsById = _.groupBy(events[0], 'id'); dedup = _.map(eventsById, eventGroup => { if (eventGroup.length > 1 && !_.every(eventGroup, isPanelAlert)) { // Get first non-panel alert diff --git a/public/app/features/annotations/specs/annotations_srv.test.ts b/public/app/features/annotations/specs/annotations_srv.test.ts index 7db7b6c9f05..97696767536 100644 --- a/public/app/features/annotations/specs/annotations_srv.test.ts +++ b/public/app/features/annotations/specs/annotations_srv.test.ts @@ -3,7 +3,7 @@ import 'app/features/dashboard/time_srv'; import { AnnotationsSrv } from '../annotations_srv'; describe('AnnotationsSrv', function() { - let $rootScope = { + const $rootScope = { onAppEvent: jest.fn(), }; let $q; @@ -11,7 +11,7 @@ describe('AnnotationsSrv', function() { let backendSrv; let timeSrv; - let annotationsSrv = new AnnotationsSrv($rootScope, $q, datasourceSrv, backendSrv, timeSrv); + const annotationsSrv = new AnnotationsSrv($rootScope, $q, datasourceSrv, backendSrv, timeSrv); describe('When translating the query result', () => { const annotationSource = { diff --git a/public/app/features/annotations/specs/annotations_srv_specs.test.ts b/public/app/features/annotations/specs/annotations_srv_specs.test.ts index 35def83e4a9..49457f34d05 100644 --- a/public/app/features/annotations/specs/annotations_srv_specs.test.ts +++ b/public/app/features/annotations/specs/annotations_srv_specs.test.ts @@ -24,7 +24,7 @@ describe('Annotations', () => { { id: 2, time: 2 }, ]; - let regions = makeRegions(testAnnotations, { range: range }); + const regions = makeRegions(testAnnotations, { range: range }); expect(regions).toEqual(expectedAnnotations); }); @@ -33,7 +33,7 @@ describe('Annotations', () => { testAnnotations = [{ id: 5, time: 4, regionId: 5 }]; const expectedAnnotations = [{ id: 5, regionId: 5, isRegion: true, time: 4, timeEnd: 7 }]; - let regions = makeRegions(testAnnotations, { range: range }); + const regions = makeRegions(testAnnotations, { range: range }); expect(regions).toEqual(expectedAnnotations); }); }); @@ -49,7 +49,7 @@ describe('Annotations', () => { ]; const expectedAnnotations = [{ id: 1, time: 1 }, { id: 2, time: 2 }, { id: 5, time: 5 }]; - let deduplicated = dedupAnnotations(testAnnotations); + const deduplicated = dedupAnnotations(testAnnotations); expect(deduplicated).toEqual(expectedAnnotations); }); @@ -63,7 +63,7 @@ describe('Annotations', () => { ]; const expectedAnnotations = [{ id: 1, time: 1 }, { id: 2, time: 2 }, { id: 5, time: 5 }]; - let deduplicated = dedupAnnotations(testAnnotations); + const deduplicated = dedupAnnotations(testAnnotations); expect(deduplicated).toEqual(expectedAnnotations); }); }); diff --git a/public/app/features/dashboard/ad_hoc_filters.ts b/public/app/features/dashboard/ad_hoc_filters.ts index ee57db23675..412761dc716 100644 --- a/public/app/features/dashboard/ad_hoc_filters.ts +++ b/public/app/features/dashboard/ad_hoc_filters.ts @@ -30,7 +30,7 @@ export class AdHocFiltersCtrl { if (this.variable.value && !_.isArray(this.variable.value)) { } - for (let tag of this.variable.filters) { + for (const tag of this.variable.filters) { if (this.segments.length > 0) { this.segments.push(this.uiSegmentSrv.newCondition('AND')); } diff --git a/public/app/features/dashboard/change_tracker.ts b/public/app/features/dashboard/change_tracker.ts index 745b76ce347..1417510bb2c 100644 --- a/public/app/features/dashboard/change_tracker.ts +++ b/public/app/features/dashboard/change_tracker.ts @@ -94,13 +94,13 @@ export class ChangeTracker { // remove stuff that should not count in diff cleanDashboardFromIgnoredChanges(dashData) { // need to new up the domain model class to get access to expand / collapse row logic - let model = new DashboardModel(dashData); + const model = new DashboardModel(dashData); // Expand all rows before making comparison. This is required because row expand / collapse // change order of panel array and panel positions. model.expandRows(); - let dash = model.getSaveModelClone(); + const dash = model.getSaveModelClone(); // ignore time and refresh dash.time = 0; @@ -138,8 +138,8 @@ export class ChangeTracker { } hasChanges() { - let current = this.cleanDashboardFromIgnoredChanges(this.current.getSaveModelClone()); - let original = this.cleanDashboardFromIgnoredChanges(this.original); + const current = this.cleanDashboardFromIgnoredChanges(this.current.getSaveModelClone()); + const original = this.cleanDashboardFromIgnoredChanges(this.original); var currentTimepicker = _.find(current.nav, { type: 'timepicker' }); var originalTimepicker = _.find(original.nav, { type: 'timepicker' }); diff --git a/public/app/features/dashboard/dashboard_import_ctrl.ts b/public/app/features/dashboard/dashboard_import_ctrl.ts index 73e9e316b4e..b70a1847602 100644 --- a/public/app/features/dashboard/dashboard_import_ctrl.ts +++ b/public/app/features/dashboard/dashboard_import_ctrl.ts @@ -51,7 +51,7 @@ export class DashboardImportCtrl { this.inputs = []; if (this.dash.__inputs) { - for (let input of this.dash.__inputs) { + for (const input of this.dash.__inputs) { var inputModel = { name: input.name, label: input.label, @@ -95,7 +95,7 @@ export class DashboardImportCtrl { inputValueChanged() { this.inputsValid = true; - for (let input of this.inputs) { + for (const input of this.inputs) { if (!input.value) { this.inputsValid = false; } diff --git a/public/app/features/dashboard/dashboard_migration.ts b/public/app/features/dashboard/dashboard_migration.ts index 1d319929bfd..3753cbe7c55 100644 --- a/public/app/features/dashboard/dashboard_migration.ts +++ b/public/app/features/dashboard/dashboard_migration.ts @@ -389,7 +389,7 @@ export class DashboardMigrator { upgradeToGridLayout(old) { let yPos = 0; - let widthFactor = GRID_COLUMN_COUNT / 12; + const widthFactor = GRID_COLUMN_COUNT / 12; const maxPanelId = _.max( _.flattenDeep( @@ -407,15 +407,15 @@ export class DashboardMigrator { // Add special "row" panels if even one row is collapsed, repeated or has visible title const showRows = _.some(old.rows, row => row.collapse || row.showTitle || row.repeat); - for (let row of old.rows) { + for (const row of old.rows) { if (row.repeatIteration) { continue; } - let height: any = row.height || DEFAULT_ROW_HEIGHT; + const height: any = row.height || DEFAULT_ROW_HEIGHT; const rowGridHeight = getGridHeight(height); - let rowPanel: any = {}; + const rowPanel: any = {}; let rowPanelModel: PanelModel; if (showRows) { // add special row panel @@ -436,9 +436,9 @@ export class DashboardMigrator { yPos++; } - let rowArea = new RowArea(rowGridHeight, GRID_COLUMN_COUNT, yPos); + const rowArea = new RowArea(rowGridHeight, GRID_COLUMN_COUNT, yPos); - for (let panel of row.panels) { + for (const panel of row.panels) { panel.span = panel.span || DEFAULT_PANEL_SPAN; if (panel.minSpan) { panel.minSpan = Math.min(GRID_COLUMN_COUNT, GRID_COLUMN_COUNT / 12 * panel.minSpan); @@ -446,7 +446,7 @@ export class DashboardMigrator { const panelWidth = Math.floor(panel.span) * widthFactor; const panelHeight = panel.height ? getGridHeight(panel.height) : rowGridHeight; - let panelPos = rowArea.getPanelPosition(panelHeight, panelWidth); + const panelPos = rowArea.getPanelPosition(panelHeight, panelWidth); yPos = rowArea.yPos; panel.gridPos = { x: panelPos.x, diff --git a/public/app/features/dashboard/dashboard_model.ts b/public/app/features/dashboard/dashboard_model.ts index 92392fc80e8..8f61cf06c60 100644 --- a/public/app/features/dashboard/dashboard_model.ts +++ b/public/app/features/dashboard/dashboard_model.ts @@ -95,7 +95,7 @@ export class DashboardModel { addBuiltInAnnotationQuery() { let found = false; - for (let item of this.annotations.list) { + for (const item of this.annotations.list) { if (item.builtIn === 1) { found = true; break; @@ -138,7 +138,7 @@ export class DashboardModel { // cleans meta data and other non persistent state getSaveModelClone(options?) { - let defaults = _.defaults(options || {}, { + const defaults = _.defaults(options || {}, { saveVariables: true, saveTimerange: true, }); @@ -160,8 +160,8 @@ export class DashboardModel { if (!defaults.saveVariables) { for (let i = 0; i < copy.templating.list.length; i++) { - let current = copy.templating.list[i]; - let original = _.find(this.originalTemplating, { name: current.name, type: current.type }); + const current = copy.templating.list[i]; + const original = _.find(this.originalTemplating, { name: current.name, type: current.type }); if (!original) { continue; @@ -213,13 +213,13 @@ export class DashboardModel { getNextPanelId() { let max = 0; - for (let panel of this.panels) { + for (const panel of this.panels) { if (panel.id > max) { max = panel.id; } if (panel.collapsed) { - for (let rowPanel of panel.panels) { + for (const rowPanel of panel.panels) { if (rowPanel.id > max) { max = rowPanel.id; } @@ -237,7 +237,7 @@ export class DashboardModel { } getPanelById(id) { - for (let panel of this.panels) { + for (const panel of this.panels) { if (panel.id === id) { return panel; } @@ -248,7 +248,7 @@ export class DashboardModel { addPanel(panelData) { panelData.id = this.getNextPanelId(); - let panel = new PanelModel(panelData); + const panel = new PanelModel(panelData); this.panels.unshift(panel); @@ -273,15 +273,15 @@ export class DashboardModel { } this.iteration = (this.iteration || new Date().getTime()) + 1; - let panelsToRemove = []; + const panelsToRemove = []; // cleanup scopedVars - for (let panel of this.panels) { + for (const panel of this.panels) { delete panel.scopedVars; } for (let i = 0; i < this.panels.length; i++) { - let panel = this.panels[i]; + const panel = this.panels[i]; if ((!panel.repeat || panel.repeatedByRow) && panel.repeatPanelId && panel.repeatIteration !== this.iteration) { panelsToRemove.push(panel); } @@ -304,7 +304,7 @@ export class DashboardModel { this.iteration = (this.iteration || new Date().getTime()) + 1; for (let i = 0; i < this.panels.length; i++) { - let panel = this.panels[i]; + const panel = this.panels[i]; if (panel.repeat) { this.repeatPanel(panel, i); } @@ -315,9 +315,9 @@ export class DashboardModel { } cleanUpRowRepeats(rowPanels) { - let panelsToRemove = []; + const panelsToRemove = []; for (let i = 0; i < rowPanels.length; i++) { - let panel = rowPanels[i]; + const panel = rowPanels[i]; if (!panel.repeat && panel.repeatPanelId) { panelsToRemove.push(panel); } @@ -333,16 +333,16 @@ export class DashboardModel { let rowPanels = row.panels; if (!row.collapsed) { - let rowPanelIndex = _.findIndex(this.panels, p => p.id === row.id); + const rowPanelIndex = _.findIndex(this.panels, p => p.id === row.id); rowPanels = this.getRowPanels(rowPanelIndex); } this.cleanUpRowRepeats(rowPanels); for (let i = 0; i < rowPanels.length; i++) { - let panel = rowPanels[i]; + const panel = rowPanels[i]; if (panel.repeat) { - let panelIndex = _.findIndex(this.panels, p => p.id === panel.id); + const panelIndex = _.findIndex(this.panels, p => p.id === panel.id); this.repeatPanel(panel, panelIndex); } } @@ -354,7 +354,7 @@ export class DashboardModel { return sourcePanel; } - let clone = new PanelModel(sourcePanel.getSaveModel()); + const clone = new PanelModel(sourcePanel.getSaveModel()); clone.id = this.getNextPanelId(); // insert after source panel + value index @@ -370,13 +370,13 @@ export class DashboardModel { // if first clone return source if (valueIndex === 0) { if (!sourceRowPanel.collapsed) { - let rowPanels = this.getRowPanels(sourcePanelIndex); + const rowPanels = this.getRowPanels(sourcePanelIndex); sourceRowPanel.panels = rowPanels; } return sourceRowPanel; } - let clone = new PanelModel(sourceRowPanel.getSaveModel()); + const clone = new PanelModel(sourceRowPanel.getSaveModel()); // for row clones we need to figure out panels under row to clone and where to insert clone let rowPanels, insertPos; if (sourceRowPanel.collapsed) { @@ -397,7 +397,7 @@ export class DashboardModel { } repeatPanel(panel: PanelModel, panelIndex: number) { - let variable = _.find(this.templating.list, { name: panel.repeat }); + const variable = _.find(this.templating.list, { name: panel.repeat }); if (!variable) { return; } @@ -407,13 +407,13 @@ export class DashboardModel { return; } - let selectedOptions = this.getSelectedVariableOptions(variable); - let minWidth = panel.minSpan || 6; + const selectedOptions = this.getSelectedVariableOptions(variable); + const minWidth = panel.minSpan || 6; let xPos = 0; let yPos = panel.gridPos.y; for (let index = 0; index < selectedOptions.length; index++) { - let option = selectedOptions[index]; + const option = selectedOptions[index]; let copy; copy = this.getPanelRepeatClone(panel, index, panelIndex); @@ -443,9 +443,9 @@ export class DashboardModel { } // Update gridPos for panels below - let yOffset = yPos - panel.gridPos.y; + const yOffset = yPos - panel.gridPos.y; if (yOffset > 0) { - let panelBelowIndex = panelIndex + selectedOptions.length; + const panelBelowIndex = panelIndex + selectedOptions.length; for (let i = panelBelowIndex; i < this.panels.length; i++) { this.panels[i].gridPos.y += yOffset; } @@ -453,7 +453,7 @@ export class DashboardModel { } repeatRow(panel: PanelModel, panelIndex: number, variable) { - let selectedOptions = this.getSelectedVariableOptions(variable); + const selectedOptions = this.getSelectedVariableOptions(variable); let yPos = panel.gridPos.y; function setScopedVars(panel, variableOption) { @@ -462,12 +462,12 @@ export class DashboardModel { } for (let optionIndex = 0; optionIndex < selectedOptions.length; optionIndex++) { - let option = selectedOptions[optionIndex]; - let rowCopy = this.getRowRepeatClone(panel, optionIndex, panelIndex); + const option = selectedOptions[optionIndex]; + const rowCopy = this.getRowRepeatClone(panel, optionIndex, panelIndex); setScopedVars(rowCopy, option); - let rowHeight = this.getRowHeight(rowCopy); - let rowPanels = rowCopy.panels || []; + const rowHeight = this.getRowHeight(rowCopy); + const rowPanels = rowCopy.panels || []; let panelBelowIndex; if (panel.collapsed) { @@ -483,11 +483,11 @@ export class DashboardModel { panelBelowIndex = panelIndex + optionIndex + 1; } else { // insert after 'row' panel - let insertPos = panelIndex + (rowPanels.length + 1) * optionIndex + 1; + const insertPos = panelIndex + (rowPanels.length + 1) * optionIndex + 1; _.each(rowPanels, (rowPanel, i) => { setScopedVars(rowPanel, option); if (optionIndex > 0) { - let cloneRowPanel = new PanelModel(rowPanel); + const cloneRowPanel = new PanelModel(rowPanel); this.updateRepeatedPanelIds(cloneRowPanel, true); // For exposed row additionally set proper Y grid position and add it to dashboard panels cloneRowPanel.gridPos.y += rowHeight * optionIndex; @@ -650,29 +650,29 @@ export class DashboardModel { formatDate(date, format?) { date = moment.isMoment(date) ? date : moment(date); format = format || 'YYYY-MM-DD HH:mm:ss'; - let timezone = this.getTimezone(); + const timezone = this.getTimezone(); return timezone === 'browser' ? moment(date).format(format) : moment.utc(date).format(format); } destroy() { this.events.removeAllListeners(); - for (let panel of this.panels) { + for (const panel of this.panels) { panel.destroy(); } } toggleRow(row: PanelModel) { - let rowIndex = _.indexOf(this.panels, row); + const rowIndex = _.indexOf(this.panels, row); if (row.collapsed) { row.collapsed = false; - let hasRepeat = _.some(row.panels, p => p.repeat); + const hasRepeat = _.some(row.panels, p => p.repeat); if (row.panels.length > 0) { // Use first panel to figure out if it was moved or pushed - let firstPanel = row.panels[0]; - let yDiff = firstPanel.gridPos.y - (row.gridPos.y + row.gridPos.h); + const firstPanel = row.panels[0]; + const yDiff = firstPanel.gridPos.y - (row.gridPos.y + row.gridPos.h); // start inserting after row let insertPos = rowIndex + 1; @@ -680,7 +680,7 @@ export class DashboardModel { // needed to know home much panels below should be pushed down let yMax = row.gridPos.y; - for (let panel of row.panels) { + for (const panel of row.panels) { // make sure y is adjusted (in case row moved while collapsed) // console.log('yDiff', yDiff); panel.gridPos.y -= yDiff; @@ -713,7 +713,7 @@ export class DashboardModel { return; } - let rowPanels = this.getRowPanels(rowIndex); + const rowPanels = this.getRowPanels(rowIndex); // remove panels _.pull(this.panels, ...rowPanels); @@ -729,10 +729,10 @@ export class DashboardModel { * Will return all panels after rowIndex until it encounters another row */ getRowPanels(rowIndex: number): PanelModel[] { - let rowPanels = []; + const rowPanels = []; for (let index = rowIndex + 1; index < this.panels.length; index++) { - let panel = this.panels[index]; + const panel = this.panels[index]; // break when encountering another row if (panel.type === 'row') { @@ -791,7 +791,7 @@ export class DashboardModel { } private updateSchema(old) { - let migrator = new DashboardMigrator(this); + const migrator = new DashboardMigrator(this); migrator.updateSchema(old); } diff --git a/public/app/features/dashboard/dashgrid/AddPanelPanel.tsx b/public/app/features/dashboard/dashgrid/AddPanelPanel.tsx index f1f2290ce40..9459fc41753 100644 --- a/public/app/features/dashboard/dashgrid/AddPanelPanel.tsx +++ b/public/app/features/dashboard/dashgrid/AddPanelPanel.tsx @@ -68,18 +68,18 @@ export class AddPanelPanel extends React.Component item) .value(); let copiedPanels = []; - let copiedPanelJson = store.get(LS_PANEL_COPY_KEY); + const copiedPanelJson = store.get(LS_PANEL_COPY_KEY); if (copiedPanelJson) { - let copiedPanel = JSON.parse(copiedPanelJson); - let pluginInfo = _.find(panels, { id: copiedPanel.type }); + const copiedPanel = JSON.parse(copiedPanelJson); + const pluginInfo = _.find(panels, { id: copiedPanel.type }); if (pluginInfo) { - let pluginCopy = _.cloneDeep(pluginInfo); + const pluginCopy = _.cloneDeep(pluginInfo); pluginCopy.name = copiedPanel.title; pluginCopy.sort = -1; pluginCopy.defaults = copiedPanel; @@ -129,7 +129,7 @@ export class AddPanelPanel extends React.Component; } @@ -156,7 +156,7 @@ export class AddPanelPanel extends React.Component { return regex.test(panel.name); }); @@ -189,12 +189,12 @@ export class AddPanelPanel extends React.Component { const layout = []; this.panelMap = {}; - for (let panel of this.dashboard.panels) { - let stringId = panel.id.toString(); + for (const panel of this.dashboard.panels) { + const stringId = panel.id.toString(); this.panelMap[stringId] = panel; if (!panel.gridPos) { @@ -103,7 +103,7 @@ export class DashboardGrid extends React.Component { continue; } - let panelPos: any = { + const panelPos: any = { i: stringId, x: panel.gridPos.x, y: panel.gridPos.y, @@ -174,7 +174,7 @@ export class DashboardGrid extends React.Component { renderPanels() { const panelElements = []; - for (let panel of this.dashboard.panels) { + for (const panel of this.dashboard.panels) { const panelClasses = classNames({ panel: true, 'panel--fullscreen': panel.fullscreen }); panelElements.push(
    diff --git a/public/app/features/dashboard/dashnav/dashnav.ts b/public/app/features/dashboard/dashnav/dashnav.ts index 628f09349d3..a83efe7d390 100644 --- a/public/app/features/dashboard/dashnav/dashnav.ts +++ b/public/app/features/dashboard/dashnav/dashnav.ts @@ -22,7 +22,7 @@ export class DashNavCtrl { } toggleSettings() { - let search = this.$location.search(); + const search = this.$location.search(); if (search.editview) { delete search.editview; } else { @@ -32,7 +32,7 @@ export class DashNavCtrl { } close() { - let search = this.$location.search(); + const search = this.$location.search(); if (search.editview) { delete search.editview; } else if (search.fullscreen) { diff --git a/public/app/features/dashboard/export/export_modal.ts b/public/app/features/dashboard/export/export_modal.ts index 2e61ce9f8a8..d314f13be4b 100644 --- a/public/app/features/dashboard/export/export_modal.ts +++ b/public/app/features/dashboard/export/export_modal.ts @@ -29,7 +29,7 @@ export class DashExportCtrl { saveJson() { var clone = this.dash; - let editScope = this.$rootScope.$new(); + const editScope = this.$rootScope.$new(); editScope.object = clone; editScope.enableCopy = true; diff --git a/public/app/features/dashboard/export/exporter.ts b/public/app/features/dashboard/export/exporter.ts index fc24de76fcc..91e9f12ae54 100644 --- a/public/app/features/dashboard/export/exporter.ts +++ b/public/app/features/dashboard/export/exporter.ts @@ -24,7 +24,7 @@ export class DashboardExporter { var promises = []; var variableLookup: any = {}; - for (let variable of saveModel.templating.list) { + for (const variable of saveModel.templating.list) { variableLookup[variable.name] = variable; } @@ -69,7 +69,7 @@ export class DashboardExporter { } if (panel.targets) { - for (let target of panel.targets) { + for (const target of panel.targets) { if (target.datasource !== undefined) { templateizeDatasourceUsage(target); } @@ -88,19 +88,19 @@ export class DashboardExporter { }; // check up panel data sources - for (let panel of saveModel.panels) { + for (const panel of saveModel.panels) { processPanel(panel); // handle collapsed rows if (panel.collapsed !== undefined && panel.collapsed === true && panel.panels) { - for (let rowPanel of panel.panels) { + for (const rowPanel of panel.panels) { processPanel(rowPanel); } } } // templatize template vars - for (let variable of saveModel.templating.list) { + for (const variable of saveModel.templating.list) { if (variable.type === 'query') { templateizeDatasourceUsage(variable); variable.options = []; @@ -110,7 +110,7 @@ export class DashboardExporter { } // templatize annotations vars - for (let annotationDef of saveModel.annotations.list) { + for (const annotationDef of saveModel.annotations.list) { templateizeDatasourceUsage(annotationDef); } @@ -129,7 +129,7 @@ export class DashboardExporter { }); // templatize constants - for (let variable of saveModel.templating.list) { + for (const variable of saveModel.templating.list) { if (variable.type === 'constant') { var refName = 'VAR_' + variable.name.replace(' ', '_').toUpperCase(); inputs.push({ diff --git a/public/app/features/dashboard/history/history.ts b/public/app/features/dashboard/history/history.ts index be6ad5af1ba..3563ccc7766 100644 --- a/public/app/features/dashboard/history/history.ts +++ b/public/app/features/dashboard/history/history.ts @@ -67,7 +67,7 @@ export class HistoryListCtrl { } revisionSelectionChanged() { - let selected = _.filter(this.revisions, { checked: true }).length; + const selected = _.filter(this.revisions, { checked: true }).length; this.canCompare = selected === 2; } @@ -134,7 +134,7 @@ export class HistoryListCtrl { .getHistoryList(this.dashboard, options) .then(revisions => { // set formatted dates & default values - for (let rev of revisions) { + for (const rev of revisions) { rev.createdDateString = this.formatDate(rev.created); rev.ageString = this.formatBasicDate(rev.created); rev.checked = false; diff --git a/public/app/features/dashboard/settings/settings.ts b/public/app/features/dashboard/settings/settings.ts index 457cac5af72..1d4a70d42aa 100755 --- a/public/app/features/dashboard/settings/settings.ts +++ b/public/app/features/dashboard/settings/settings.ts @@ -109,7 +109,7 @@ export class SettingsCtrl { const params = this.$location.search(); const url = this.$location.path(); - for (let section of this.sections) { + for (const section of this.sections) { const sectionParams = _.defaults({ editview: section.id }, params); section.url = config.appSubUrl + url + '?' + $.param(sectionParams); } diff --git a/public/app/features/dashboard/shareModalCtrl.ts b/public/app/features/dashboard/shareModalCtrl.ts index c32c2a79190..fff307c2510 100644 --- a/public/app/features/dashboard/shareModalCtrl.ts +++ b/public/app/features/dashboard/shareModalCtrl.ts @@ -91,7 +91,7 @@ export function ShareModalCtrl($scope, $rootScope, $location, $timeout, timeSrv, // This function will try to return the proper full name of the local timezone // Chrome does not handle the timezone offset (but phantomjs does) $scope.getLocalTimeZone = function() { - let utcOffset = '&tz=UTC' + encodeURIComponent(moment().format('Z')); + const utcOffset = '&tz=UTC' + encodeURIComponent(moment().format('Z')); // Older browser does not the internationalization API if (!(window).Intl) { diff --git a/public/app/features/dashboard/specs/dashboard_migration.test.ts b/public/app/features/dashboard/specs/dashboard_migration.test.ts index 07a29d58e65..f440dbb49f2 100644 --- a/public/app/features/dashboard/specs/dashboard_migration.test.ts +++ b/public/app/features/dashboard/specs/dashboard_migration.test.ts @@ -151,18 +151,18 @@ describe('DashboardModel', function() { it('should create proper grid', function() { model.rows = [createRow({ collapse: false, height: 8 }, [[6], [6]])]; - let dashboard = new DashboardModel(model); - let panelGridPos = getGridPositions(dashboard); - let expectedGrid = [{ x: 0, y: 0, w: 12, h: 8 }, { x: 12, y: 0, w: 12, h: 8 }]; + const dashboard = new DashboardModel(model); + const panelGridPos = getGridPositions(dashboard); + const expectedGrid = [{ x: 0, y: 0, w: 12, h: 8 }, { x: 12, y: 0, w: 12, h: 8 }]; expect(panelGridPos).toEqual(expectedGrid); }); it('should add special "row" panel if row is collapsed', function() { model.rows = [createRow({ collapse: true, height: 8 }, [[6], [6]]), createRow({ height: 8 }, [[12]])]; - let dashboard = new DashboardModel(model); - let panelGridPos = getGridPositions(dashboard); - let expectedGrid = [ + const dashboard = new DashboardModel(model); + const panelGridPos = getGridPositions(dashboard); + const expectedGrid = [ { x: 0, y: 0, w: 24, h: 8 }, // row { x: 0, y: 1, w: 24, h: 8 }, // row { x: 0, y: 2, w: 24, h: 8 }, @@ -176,9 +176,9 @@ describe('DashboardModel', function() { createRow({ showTitle: true, title: 'Row', height: 8 }, [[6], [6]]), createRow({ height: 8 }, [[12]]), ]; - let dashboard = new DashboardModel(model); - let panelGridPos = getGridPositions(dashboard); - let expectedGrid = [ + const dashboard = new DashboardModel(model); + const panelGridPos = getGridPositions(dashboard); + const expectedGrid = [ { x: 0, y: 0, w: 24, h: 8 }, // row { x: 0, y: 1, w: 12, h: 8 }, { x: 12, y: 1, w: 12, h: 8 }, @@ -196,9 +196,9 @@ describe('DashboardModel', function() { createRow({ height: 8 }, [[12], [6], [6]]), createRow({ collapse: true, height: 8 }, [[12]]), ]; - let dashboard = new DashboardModel(model); - let panelGridPos = getGridPositions(dashboard); - let expectedGrid = [ + const dashboard = new DashboardModel(model); + const panelGridPos = getGridPositions(dashboard); + const expectedGrid = [ { x: 0, y: 0, w: 24, h: 8 }, // row { x: 0, y: 1, w: 24, h: 8 }, // row { x: 0, y: 2, w: 24, h: 8 }, @@ -214,9 +214,9 @@ describe('DashboardModel', function() { it('should add all rows if even one collapsed or titled row is present', function() { model.rows = [createRow({ collapse: true, height: 8 }, [[6], [6]]), createRow({ height: 8 }, [[12]])]; - let dashboard = new DashboardModel(model); - let panelGridPos = getGridPositions(dashboard); - let expectedGrid = [ + const dashboard = new DashboardModel(model); + const panelGridPos = getGridPositions(dashboard); + const expectedGrid = [ { x: 0, y: 0, w: 24, h: 8 }, // row { x: 0, y: 1, w: 24, h: 8 }, // row { x: 0, y: 2, w: 24, h: 8 }, @@ -230,9 +230,9 @@ describe('DashboardModel', function() { createRow({ height: 6 }, [[6], [6, 3], [6, 3]]), createRow({ height: 6 }, [[4], [4], [4, 3], [4, 3]]), ]; - let dashboard = new DashboardModel(model); - let panelGridPos = getGridPositions(dashboard); - let expectedGrid = [ + const dashboard = new DashboardModel(model); + const panelGridPos = getGridPositions(dashboard); + const expectedGrid = [ { x: 0, y: 0, w: 12, h: 6 }, { x: 12, y: 0, w: 12, h: 3 }, { x: 12, y: 3, w: 12, h: 3 }, @@ -247,9 +247,9 @@ describe('DashboardModel', function() { it('should place panel to the right side of panel having bigger height', function() { model.rows = [createRow({ height: 6 }, [[4], [2, 3], [4, 6], [2, 3], [2, 3]])]; - let dashboard = new DashboardModel(model); - let panelGridPos = getGridPositions(dashboard); - let expectedGrid = [ + const dashboard = new DashboardModel(model); + const panelGridPos = getGridPositions(dashboard); + const expectedGrid = [ { x: 0, y: 0, w: 8, h: 6 }, { x: 8, y: 0, w: 4, h: 3 }, { x: 12, y: 0, w: 8, h: 6 }, @@ -262,9 +262,9 @@ describe('DashboardModel', function() { it('should fill current row if it possible', function() { model.rows = [createRow({ height: 9 }, [[4], [2, 3], [4, 6], [2, 3], [2, 3], [8, 3]])]; - let dashboard = new DashboardModel(model); - let panelGridPos = getGridPositions(dashboard); - let expectedGrid = [ + const dashboard = new DashboardModel(model); + const panelGridPos = getGridPositions(dashboard); + const expectedGrid = [ { x: 0, y: 0, w: 8, h: 9 }, { x: 8, y: 0, w: 4, h: 3 }, { x: 12, y: 0, w: 8, h: 6 }, @@ -278,9 +278,9 @@ describe('DashboardModel', function() { it('should fill current row if it possible (2)', function() { model.rows = [createRow({ height: 8 }, [[4], [2, 3], [4, 6], [2, 3], [2, 3], [8, 3]])]; - let dashboard = new DashboardModel(model); - let panelGridPos = getGridPositions(dashboard); - let expectedGrid = [ + const dashboard = new DashboardModel(model); + const panelGridPos = getGridPositions(dashboard); + const expectedGrid = [ { x: 0, y: 0, w: 8, h: 8 }, { x: 8, y: 0, w: 4, h: 3 }, { x: 12, y: 0, w: 8, h: 6 }, @@ -294,9 +294,9 @@ describe('DashboardModel', function() { it('should fill current row if panel height more than row height', function() { model.rows = [createRow({ height: 6 }, [[4], [2, 3], [4, 8], [2, 3], [2, 3]])]; - let dashboard = new DashboardModel(model); - let panelGridPos = getGridPositions(dashboard); - let expectedGrid = [ + const dashboard = new DashboardModel(model); + const panelGridPos = getGridPositions(dashboard); + const expectedGrid = [ { x: 0, y: 0, w: 8, h: 6 }, { x: 8, y: 0, w: 4, h: 3 }, { x: 12, y: 0, w: 8, h: 8 }, @@ -309,9 +309,9 @@ describe('DashboardModel', function() { it('should wrap panels to multiple rows', function() { model.rows = [createRow({ height: 6 }, [[6], [6], [12], [6], [3], [3]])]; - let dashboard = new DashboardModel(model); - let panelGridPos = getGridPositions(dashboard); - let expectedGrid = [ + const dashboard = new DashboardModel(model); + const panelGridPos = getGridPositions(dashboard); + const expectedGrid = [ { x: 0, y: 0, w: 12, h: 6 }, { x: 12, y: 0, w: 12, h: 6 }, { x: 0, y: 6, w: 24, h: 6 }, @@ -328,9 +328,9 @@ describe('DashboardModel', function() { createRow({ showTitle: true, title: 'Row', height: 8, repeat: 'server' }, [[6]]), createRow({ height: 8 }, [[12]]), ]; - let dashboard = new DashboardModel(model); - let panelGridPos = getGridPositions(dashboard); - let expectedGrid = [ + const dashboard = new DashboardModel(model); + const panelGridPos = getGridPositions(dashboard); + const expectedGrid = [ { x: 0, y: 0, w: 24, h: 8 }, { x: 0, y: 1, w: 12, h: 8 }, { x: 0, y: 9, w: 24, h: 8 }, @@ -359,7 +359,7 @@ describe('DashboardModel', function() { ), ]; - let dashboard = new DashboardModel(model); + const dashboard = new DashboardModel(model); expect(dashboard.panels[0].repeat).toBe('server'); expect(dashboard.panels.length).toBe(2); }); @@ -368,7 +368,7 @@ describe('DashboardModel', function() { model.rows = [createRow({ height: 8 }, [[6]])]; model.rows[0].panels[0] = { minSpan: 12 }; - let dashboard = new DashboardModel(model); + const dashboard = new DashboardModel(model); expect(dashboard.panels[0].minSpan).toBe(24); }); @@ -376,7 +376,7 @@ describe('DashboardModel', function() { model.rows = [createRow({ collapse: true, height: 8 }, [[6], [6]])]; model.rows[0].panels[0] = {}; - let dashboard = new DashboardModel(model); + const dashboard = new DashboardModel(model); expect(dashboard.panels[0].id).toBe(1); }); }); @@ -386,15 +386,15 @@ function createRow(options, panelDescriptions: any[]) { const PANEL_HEIGHT_STEP = GRID_CELL_HEIGHT + GRID_CELL_VMARGIN; let { collapse, height, showTitle, title, repeat, repeatIteration } = options; height = height * PANEL_HEIGHT_STEP; - let panels = []; + const panels = []; _.each(panelDescriptions, panelDesc => { - let panel = { span: panelDesc[0] }; + const panel = { span: panelDesc[0] }; if (panelDesc.length > 1) { panel['height'] = panelDesc[1] * PANEL_HEIGHT_STEP; } panels.push(panel); }); - let row = { + const row = { collapse, height, showTitle, diff --git a/public/app/features/dashboard/specs/dashboard_model.test.ts b/public/app/features/dashboard/specs/dashboard_model.test.ts index 6ac642cd58e..28029653a6c 100644 --- a/public/app/features/dashboard/specs/dashboard_model.test.ts +++ b/public/app/features/dashboard/specs/dashboard_model.test.ts @@ -457,16 +457,16 @@ describe('DashboardModel', function() { }); it('getSaveModelClone should return original time when saveTimerange=false', () => { - let options = { saveTimerange: false }; - let saveModel = model.getSaveModelClone(options); + const options = { saveTimerange: false }; + const saveModel = model.getSaveModelClone(options); expect(saveModel.time.from).toBe('now-6h'); expect(saveModel.time.to).toBe('now'); }); it('getSaveModelClone should return updated time when saveTimerange=true', () => { - let options = { saveTimerange: true }; - let saveModel = model.getSaveModelClone(options); + const options = { saveTimerange: true }; + const saveModel = model.getSaveModelClone(options); expect(saveModel.time.from).toBe('now-3h'); expect(saveModel.time.to).toBe('now-1h'); @@ -478,16 +478,16 @@ describe('DashboardModel', function() { }); it('getSaveModelClone should return original time when saveTimerange=false', () => { - let options = { saveTimerange: false }; - let saveModel = model.getSaveModelClone(options); + const options = { saveTimerange: false }; + const saveModel = model.getSaveModelClone(options); expect(saveModel.time.from).toBe('now-6h'); expect(saveModel.time.to).toBe('now'); }); it('getSaveModelClone should return updated time when saveTimerange=true', () => { - let options = { saveTimerange: true }; - let saveModel = model.getSaveModelClone(options); + const options = { saveTimerange: true }; + const saveModel = model.getSaveModelClone(options); expect(saveModel.time.from).toBe('now-3h'); expect(saveModel.time.to).toBe('now-1h'); @@ -542,8 +542,8 @@ describe('DashboardModel', function() { it('getSaveModelClone should return original variable when saveVariables=false', () => { model.templating.list[0].current.text = 'server_002'; - let options = { saveVariables: false }; - let saveModel = model.getSaveModelClone(options); + const options = { saveVariables: false }; + const saveModel = model.getSaveModelClone(options); expect(saveModel.templating.list[0].current.text).toBe('server_001'); }); @@ -551,8 +551,8 @@ describe('DashboardModel', function() { it('getSaveModelClone should return updated variable when saveVariables=true', () => { model.templating.list[0].current.text = 'server_002'; - let options = { saveVariables: true }; - let saveModel = model.getSaveModelClone(options); + const options = { saveVariables: true }; + const saveModel = model.getSaveModelClone(options); expect(saveModel.templating.list[0].current.text).toBe('server_002'); }); @@ -620,8 +620,8 @@ describe('DashboardModel', function() { it('getSaveModelClone should return original variable when saveVariables=false', () => { model.templating.list[0].filters[0].value = 'server 1'; - let options = { saveVariables: false }; - let saveModel = model.getSaveModelClone(options); + const options = { saveVariables: false }; + const saveModel = model.getSaveModelClone(options); expect(saveModel.templating.list[0].filters[0].value).toBe('server 20'); }); @@ -629,8 +629,8 @@ describe('DashboardModel', function() { it('getSaveModelClone should return updated variable when saveVariables=true', () => { model.templating.list[0].filters[0].value = 'server 1'; - let options = { saveVariables: true }; - let saveModel = model.getSaveModelClone(options); + const options = { saveVariables: true }; + const saveModel = model.getSaveModelClone(options); expect(saveModel.templating.list[0].filters[0].value).toBe('server 1'); }); diff --git a/public/app/features/dashboard/specs/history_ctrl.test.ts b/public/app/features/dashboard/specs/history_ctrl.test.ts index 991ecb2c60d..632f3489dae 100644 --- a/public/app/features/dashboard/specs/history_ctrl.test.ts +++ b/public/app/features/dashboard/specs/history_ctrl.test.ts @@ -70,7 +70,7 @@ describe('HistoryListCtrl', () => { }); it('should add a checked property to each revision', () => { - let actual = _.filter(historyListCtrl.revisions, rev => rev.hasOwnProperty('checked')); + const actual = _.filter(historyListCtrl.revisions, rev => rev.hasOwnProperty('checked')); expect(actual.length).toBe(4); }); @@ -78,7 +78,7 @@ describe('HistoryListCtrl', () => { historyListCtrl.revisions[0].checked = true; historyListCtrl.revisions[2].checked = true; historyListCtrl.reset(); - let actual = _.filter(historyListCtrl.revisions, rev => !rev.checked); + const actual = _.filter(historyListCtrl.revisions, rev => !rev.checked); expect(actual.length).toBe(4); }); }); diff --git a/public/app/features/dashboard/specs/history_srv.test.ts b/public/app/features/dashboard/specs/history_srv.test.ts index 401b098a0e1..5c8578ecf39 100644 --- a/public/app/features/dashboard/specs/history_srv.test.ts +++ b/public/app/features/dashboard/specs/history_srv.test.ts @@ -8,7 +8,7 @@ describe('historySrv', function() { const versionsResponse = versions(); const restoreResponse = restore; - let backendSrv = { + const backendSrv = { get: jest.fn(() => Promise.resolve({})), post: jest.fn(() => Promise.resolve({})), }; @@ -44,7 +44,7 @@ describe('historySrv', function() { describe('restoreDashboard', () => { it('should return a success response given valid parameters', function() { - let version = 6; + const version = 6; backendSrv.post = jest.fn(() => Promise.resolve(restoreResponse(version))); historySrv = new HistorySrv(backendSrv); return historySrv.restoreDashboard(dash, version).then(function(response) { @@ -54,7 +54,7 @@ describe('historySrv', function() { it('should return an empty object when not given an id', async () => { historySrv = new HistorySrv(backendSrv); - let rsp = await historySrv.restoreDashboard(emptyDash, 6); + const rsp = await historySrv.restoreDashboard(emptyDash, 6); expect(rsp).toEqual({}); }); }); diff --git a/public/app/features/dashboard/specs/repeat.test.ts b/public/app/features/dashboard/specs/repeat.test.ts index 09bb3b7c494..d8c9e3bc2ed 100644 --- a/public/app/features/dashboard/specs/repeat.test.ts +++ b/public/app/features/dashboard/specs/repeat.test.ts @@ -8,7 +8,7 @@ describe('given dashboard with panel repeat', function() { var dashboard; beforeEach(function() { - let dashboardJSON = { + const dashboardJSON = { panels: [ { id: 1, type: 'row', gridPos: { x: 0, y: 0, h: 1, w: 24 } }, { id: 2, repeat: 'apps', repeatDirection: 'h', gridPos: { x: 0, y: 1, h: 2, w: 8 } }, diff --git a/public/app/features/dashboard/specs/viewstate_srv.test.ts b/public/app/features/dashboard/specs/viewstate_srv.test.ts index 08166c6f2bd..740e3c3b9a8 100644 --- a/public/app/features/dashboard/specs/viewstate_srv.test.ts +++ b/public/app/features/dashboard/specs/viewstate_srv.test.ts @@ -4,12 +4,12 @@ import config from 'app/core/config'; import { DashboardViewState } from '../view_state_srv'; describe('when updating view state', () => { - let location = { + const location = { replace: jest.fn(), search: jest.fn(), }; - let $scope = { + const $scope = { onAppEvent: jest.fn(() => {}), dashboard: { meta: {}, @@ -17,7 +17,7 @@ describe('when updating view state', () => { }, }; - let $rootScope = {}; + const $rootScope = {}; let viewState; beforeEach(() => { diff --git a/public/app/features/dashboard/validation_srv.ts b/public/app/features/dashboard/validation_srv.ts index 817be7ca0e3..3e8306039d7 100644 --- a/public/app/features/dashboard/validation_srv.ts +++ b/public/app/features/dashboard/validation_srv.ts @@ -37,7 +37,7 @@ export class ValidationSrv { }); } - let deferred = this.$q.defer(); + const deferred = this.$q.defer(); const promises = []; promises.push(this.backendSrv.search({ type: hitTypes.FOLDER, folderIds: [folderId], query: name })); @@ -54,7 +54,7 @@ export class ValidationSrv { hits = hits.concat(res[1]); } - for (let hit of hits) { + for (const hit of hits) { if (nameLowerCased === hit.title.toLowerCase()) { deferred.reject({ type: 'EXISTING', diff --git a/public/app/features/dashboard/view_state_srv.ts b/public/app/features/dashboard/view_state_srv.ts index 1ed2d61df71..5bd4db6fddc 100644 --- a/public/app/features/dashboard/view_state_srv.ts +++ b/public/app/features/dashboard/view_state_srv.ts @@ -111,9 +111,9 @@ export class DashboardViewState { } toggleCollapsedPanelRow(panelId) { - for (let panel of this.dashboard.panels) { + for (const panel of this.dashboard.panels) { if (panel.collapsed) { - for (let rowPanel of panel.panels) { + for (const rowPanel of panel.panels) { if (rowPanel.id === panelId) { this.dashboard.toggleRow(panel); return; diff --git a/public/app/features/org/org_users_ctrl.ts b/public/app/features/org/org_users_ctrl.ts index d35b967626a..625e2749399 100644 --- a/public/app/features/org/org_users_ctrl.ts +++ b/public/app/features/org/org_users_ctrl.ts @@ -44,7 +44,7 @@ export class OrgUsersCtrl { } onQueryUpdated() { - let regex = new RegExp(this.searchQuery, 'ig'); + const regex = new RegExp(this.searchQuery, 'ig'); this.users = _.filter(this.unfiltered, item => { return regex.test(item.email) || regex.test(item.login); }); diff --git a/public/app/features/panel/metrics_tab.ts b/public/app/features/panel/metrics_tab.ts index 4da40f214a1..94aa142a6b8 100644 --- a/public/app/features/panel/metrics_tab.ts +++ b/public/app/features/panel/metrics_tab.ts @@ -28,7 +28,7 @@ export class MetricsTabCtrl { this.datasources = datasourceSrv.getMetricSources(); this.panelDsValue = this.panelCtrl.panel.datasource; - for (let ds of this.datasources) { + for (const ds of this.datasources) { if (ds.value === this.panelDsValue) { this.datasourceInstance = ds; } diff --git a/public/app/features/panel/panel_ctrl.ts b/public/app/features/panel/panel_ctrl.ts index 6402227164f..6a583b700ef 100644 --- a/public/app/features/panel/panel_ctrl.ts +++ b/public/app/features/panel/panel_ctrl.ts @@ -138,7 +138,7 @@ export class PanelCtrl { } getMenu() { - let menu = []; + const menu = []; menu.push({ text: 'View', click: 'ctrl.viewPanel();', @@ -166,7 +166,7 @@ export class PanelCtrl { // Additional items from sub-class menu.push(...this.getAdditionalMenuItems()); - let extendedMenu = this.getExtendedMenu(); + const extendedMenu = this.getExtendedMenu(); menu.push({ text: 'More ...', click: '', @@ -189,7 +189,7 @@ export class PanelCtrl { } getExtendedMenu() { - let menu = []; + const menu = []; if (!this.fullscreen && this.dashboard.meta.canEdit) { menu.push({ text: 'Duplicate', @@ -259,7 +259,7 @@ export class PanelCtrl { } editPanelJson() { - let editScope = this.$scope.$root.$new(); + const editScope = this.$scope.$root.$new(); editScope.object = this.panel.getSaveModel(); editScope.updateHandler = this.replacePanel.bind(this); editScope.enableCopy = true; @@ -276,12 +276,12 @@ export class PanelCtrl { } replacePanel(newPanel, oldPanel) { - let dashboard = this.dashboard; - let index = _.findIndex(dashboard.panels, panel => { + const dashboard = this.dashboard; + const index = _.findIndex(dashboard.panels, panel => { return panel.id === oldPanel.id; }); - let deletedPanel = dashboard.panels.splice(index, 1); + const deletedPanel = dashboard.panels.splice(index, 1); this.dashboard.events.emit('panel-removed', deletedPanel); newPanel = new PanelModel(newPanel); @@ -333,7 +333,7 @@ export class PanelCtrl { if (this.panel.links && this.panel.links.length > 0) { html += ''; @@ -73,7 +73,7 @@ function renderMenuItem(item, ctrl) { function createMenuTemplate(ctrl) { let html = ''; - for (let item of ctrl.getMenu()) { + for (const item of ctrl.getMenu()) { html += renderMenuItem(item, ctrl); } @@ -86,7 +86,7 @@ function panelHeader($compile) { restrict: 'E', template: template, link: function(scope, elem, attrs) { - let menuElem = elem.find('.panel-menu'); + const menuElem = elem.find('.panel-menu'); let menuScope; let isDragged; @@ -99,7 +99,7 @@ function panelHeader($compile) { } menuScope = scope.$new(); - let menuHtml = createMenuTemplate(scope.ctrl); + const menuHtml = createMenuTemplate(scope.ctrl); menuElem.html(menuHtml); $compile(menuElem)(menuScope); @@ -132,12 +132,12 @@ function panelHeader($compile) { .find('[data-toggle=dropdown]') .parentsUntil('.panel') .parent(); - let menuElem = elem.find('[data-toggle=dropdown]').parent(); + const menuElem = elem.find('[data-toggle=dropdown]').parent(); panelElem = panelElem && panelElem.length ? panelElem[0] : undefined; if (panelElem) { panelElem = $(panelElem); $(panelGridClass).removeClass(menuOpenClass); - let state = !menuElem.hasClass('open'); + const state = !menuElem.hasClass('open'); panelElem.toggleClass(menuOpenClass, state); } } diff --git a/public/app/features/panel/solo_panel_ctrl.ts b/public/app/features/panel/solo_panel_ctrl.ts index 242d2e7da3e..85773a3a778 100644 --- a/public/app/features/panel/solo_panel_ctrl.ts +++ b/public/app/features/panel/solo_panel_ctrl.ts @@ -34,7 +34,7 @@ export class SoloPanelCtrl { }; $scope.initPanelScope = function() { - let panelInfo = $scope.dashboard.getPanelInfoById(panelId); + const panelInfo = $scope.dashboard.getPanelInfoById(panelId); // fake row ctrl scope $scope.ctrl = { diff --git a/public/app/features/panellinks/specs/link_srv.test.ts b/public/app/features/panellinks/specs/link_srv.test.ts index 2ec38961e29..521a4edef15 100644 --- a/public/app/features/panellinks/specs/link_srv.test.ts +++ b/public/app/features/panellinks/specs/link_srv.test.ts @@ -2,7 +2,7 @@ import { LinkSrv } from '../link_srv'; import _ from 'lodash'; jest.mock('angular', () => { - let AngularJSMock = require('test/mocks/angular'); + const AngularJSMock = require('test/mocks/angular'); return new AngularJSMock(); }); diff --git a/public/app/features/playlist/playlist_routes.ts b/public/app/features/playlist/playlist_routes.ts index b898820e371..3cb9aceaefb 100644 --- a/public/app/features/playlist/playlist_routes.ts +++ b/public/app/features/playlist/playlist_routes.ts @@ -24,7 +24,7 @@ function grafanaRoutes($routeProvider) { controller: 'PlaylistsCtrl', resolve: { init: function(playlistSrv, $route) { - let playlistId = $route.current.params.id; + const playlistId = $route.current.params.id; playlistSrv.start(playlistId); }, }, diff --git a/public/app/features/playlist/specs/playlist_edit_ctrl.test.ts b/public/app/features/playlist/specs/playlist_edit_ctrl.test.ts index f313c6e8e6a..183947f5072 100644 --- a/public/app/features/playlist/specs/playlist_edit_ctrl.test.ts +++ b/public/app/features/playlist/specs/playlist_edit_ctrl.test.ts @@ -4,7 +4,7 @@ import { PlaylistEditCtrl } from '../playlist_edit_ctrl'; describe('PlaylistEditCtrl', () => { var ctx: any; beforeEach(() => { - let navModelSrv = { + const navModelSrv = { getNav: () => { return { breadcrumbs: [], node: {} }; }, diff --git a/public/app/features/plugins/ds_list_ctrl.ts b/public/app/features/plugins/ds_list_ctrl.ts index 89c760ae253..71c1a516842 100644 --- a/public/app/features/plugins/ds_list_ctrl.ts +++ b/public/app/features/plugins/ds_list_ctrl.ts @@ -17,7 +17,7 @@ export class DataSourcesCtrl { } onQueryUpdated() { - let regex = new RegExp(this.searchQuery, 'ig'); + const regex = new RegExp(this.searchQuery, 'ig'); this.datasources = _.filter(this.unfiltered, item => { regex.lastIndex = 0; return regex.test(item.name) || regex.test(item.type); diff --git a/public/app/features/plugins/plugin_component.ts b/public/app/features/plugins/plugin_component.ts index 1936e57f558..bdfb47bc861 100644 --- a/public/app/features/plugins/plugin_component.ts +++ b/public/app/features/plugins/plugin_component.ts @@ -68,7 +68,7 @@ function pluginDirectiveLoader($compile, datasourceSrv, $rootScope, $q, $http, $ }, }; - let panelInfo = config.panels[scope.panel.type]; + const panelInfo = config.panels[scope.panel.type]; var panelCtrlPromise = Promise.resolve(UnknownPanelCtrl); if (panelInfo) { panelCtrlPromise = importPluginModule(panelInfo.module).then(function(panelModule) { @@ -107,7 +107,7 @@ function pluginDirectiveLoader($compile, datasourceSrv, $rootScope, $q, $http, $ switch (attrs.type) { // QueryCtrl case 'query-ctrl': { - let datasource = scope.target.datasource || scope.ctrl.panel.datasource; + const datasource = scope.target.datasource || scope.ctrl.panel.datasource; return datasourceSrv.get(datasource).then(ds => { scope.datasource = ds; @@ -160,7 +160,7 @@ function pluginDirectiveLoader($compile, datasourceSrv, $rootScope, $q, $http, $ } // AppConfigCtrl case 'app-config-ctrl': { - let model = scope.ctrl.model; + const model = scope.ctrl.model; return importPluginModule(model.module).then(function(appModule) { return { baseUrl: model.baseUrl, @@ -173,7 +173,7 @@ function pluginDirectiveLoader($compile, datasourceSrv, $rootScope, $q, $http, $ } // App Page case 'app-page': { - let appModel = scope.ctrl.appModel; + const appModel = scope.ctrl.appModel; return importPluginModule(appModel.module).then(function(appModule) { return { baseUrl: appModel.baseUrl, diff --git a/public/app/features/plugins/plugin_edit_ctrl.ts b/public/app/features/plugins/plugin_edit_ctrl.ts index 6aa8b2bc38f..93c2008651d 100644 --- a/public/app/features/plugins/plugin_edit_ctrl.ts +++ b/public/app/features/plugins/plugin_edit_ctrl.ts @@ -53,7 +53,7 @@ export class PluginEditCtrl { url: `plugins/${this.model.id}/edit?tab=config`, }); - let hasDashboards = _.find(model.includes, { type: 'dashboard' }); + const hasDashboards = _.find(model.includes, { type: 'dashboard' }); if (hasDashboards) { this.navModel.main.children.push({ @@ -69,7 +69,7 @@ export class PluginEditCtrl { this.tab = this.$routeParams.tab || defaultTab; - for (let tab of this.navModel.main.children) { + for (const tab of this.navModel.main.children) { if (tab.id === this.tab) { tab.active = true; } @@ -98,7 +98,7 @@ export class PluginEditCtrl { initReadme() { return this.backendSrv.get(`/api/plugins/${this.pluginId}/markdown/readme`).then(res => { var md = new Remarkable({ - linkify: true + linkify: true, }); this.readmeHtml = this.$sce.trustAsHtml(md.render(res)); }); diff --git a/public/app/features/plugins/plugin_list_ctrl.ts b/public/app/features/plugins/plugin_list_ctrl.ts index 8e303143946..315252364cc 100644 --- a/public/app/features/plugins/plugin_list_ctrl.ts +++ b/public/app/features/plugins/plugin_list_ctrl.ts @@ -20,7 +20,7 @@ export class PluginListCtrl { } onQueryUpdated() { - let regex = new RegExp(this.searchQuery, 'ig'); + const regex = new RegExp(this.searchQuery, 'ig'); this.plugins = _.filter(this.allPlugins, item => { return regex.test(item.name) || regex.test(item.type); }); diff --git a/public/app/features/plugins/plugin_loader.ts b/public/app/features/plugins/plugin_loader.ts index cce494d0a60..e227dbb910c 100644 --- a/public/app/features/plugins/plugin_loader.ts +++ b/public/app/features/plugins/plugin_loader.ts @@ -140,12 +140,12 @@ const flotDeps = [ 'jquery.flot.events', 'jquery.flot.gauge', ]; -for (let flotDep of flotDeps) { +for (const flotDep of flotDeps) { exposeToPlugin(flotDep, { fakeDep: 1 }); } export function importPluginModule(path: string): Promise { - let builtIn = builtInPlugins[path]; + const builtIn = builtInPlugins[path]; if (builtIn) { return Promise.resolve(builtIn); } diff --git a/public/app/features/plugins/plugin_page_ctrl.ts b/public/app/features/plugins/plugin_page_ctrl.ts index 397916aacc8..a2920e55a2a 100644 --- a/public/app/features/plugins/plugin_page_ctrl.ts +++ b/public/app/features/plugins/plugin_page_ctrl.ts @@ -33,7 +33,7 @@ export class AppPageCtrl { return; } - let pluginNav = this.navModelSrv.getNav('plugin-page-' + app.id); + const pluginNav = this.navModelSrv.getNav('plugin-page-' + app.id); this.navModel = { main: { diff --git a/public/app/features/plugins/specs/datasource_srv.test.ts b/public/app/features/plugins/specs/datasource_srv.test.ts index b63e8537837..653e431cb9f 100644 --- a/public/app/features/plugins/specs/datasource_srv.test.ts +++ b/public/app/features/plugins/specs/datasource_srv.test.ts @@ -16,7 +16,7 @@ const templateSrv = { }; describe('datasource_srv', function() { - let _datasourceSrv = new DatasourceSrv({}, {}, {}, templateSrv); + const _datasourceSrv = new DatasourceSrv({}, {}, {}, templateSrv); describe('when loading explore sources', () => { beforeEach(() => { @@ -46,7 +46,7 @@ describe('datasource_srv', function() { describe('when loading metric sources', () => { let metricSources; - let unsortedDatasources = { + const unsortedDatasources = { mmm: { type: 'test-db', meta: { metrics: { m: 1 } }, diff --git a/public/app/features/templating/specs/editor_ctrl.test.ts b/public/app/features/templating/specs/editor_ctrl.test.ts index f49d0ccd9c6..bba175c2d86 100644 --- a/public/app/features/templating/specs/editor_ctrl.test.ts +++ b/public/app/features/templating/specs/editor_ctrl.test.ts @@ -9,7 +9,7 @@ jest.mock('app/core/app_events', () => { }); describe('VariableEditorCtrl', () => { - let scope = { + const scope = { runQuery: () => { return Promise.resolve({}); }, diff --git a/public/app/features/templating/specs/variable_srv_init.test.ts b/public/app/features/templating/specs/variable_srv_init.test.ts index ea8689f528b..e011d4d0d15 100644 --- a/public/app/features/templating/specs/variable_srv_init.test.ts +++ b/public/app/features/templating/specs/variable_srv_init.test.ts @@ -5,7 +5,7 @@ import { VariableSrv } from '../variable_srv'; import $q from 'q'; describe('VariableSrv init', function() { - let templateSrv = { + const templateSrv = { init: vars => { this.variables = vars; }, @@ -17,8 +17,8 @@ describe('VariableSrv init', function() { }), }; - let $injector = {}; - let $rootscope = { + const $injector = {}; + const $rootscope = { $on: () => {}, }; diff --git a/public/app/features/templating/variable_srv.ts b/public/app/features/templating/variable_srv.ts index bd214639552..e3e75d6a036 100644 --- a/public/app/features/templating/variable_srv.ts +++ b/public/app/features/templating/variable_srv.ts @@ -23,7 +23,7 @@ export class VariableSrv { this.templateSrv.init(this.variables); // init variables - for (let variable of this.variables) { + for (const variable of this.variables) { variable.initLock = this.$q.defer(); } @@ -60,7 +60,7 @@ export class VariableSrv { processVariable(variable, queryParams) { var dependencies = []; - for (let otherVariable of this.variables) { + for (const otherVariable of this.variables) { if (variable.dependsOn(otherVariable)) { dependencies.push(otherVariable.initLock.promise); } @@ -212,13 +212,13 @@ export class VariableSrv { }); let defaultText = urlValue; - let defaultValue = urlValue; + const defaultValue = urlValue; if (!option && _.isArray(urlValue)) { defaultText = []; for (let n = 0; n < urlValue.length; n++) { - let t = _.find(variable.options, op => { + const t = _.find(variable.options, op => { return op.value === urlValue[n]; }); @@ -275,7 +275,7 @@ export class VariableSrv { this.addVariable(variable); } - let filters = variable.filters; + const filters = variable.filters; let filter = _.find(filters, { key: options.key, value: options.value }); if (!filter) { @@ -288,7 +288,7 @@ export class VariableSrv { } createGraph() { - let g = new Graph(); + const g = new Graph(); this.variables.forEach(v1 => { g.createNode(v1.name); diff --git a/public/app/plugins/datasource/cloudwatch/datasource.ts b/public/app/plugins/datasource/cloudwatch/datasource.ts index 087bd19da71..63a35e72add 100644 --- a/public/app/plugins/datasource/cloudwatch/datasource.ts +++ b/public/app/plugins/datasource/cloudwatch/datasource.ts @@ -45,7 +45,7 @@ export default class CloudWatchDatasource { item.returnData = typeof item.hide === 'undefined' ? true : !item.hide; // valid ExtendedStatistics is like p90.00, check the pattern - let hasInvalidStatistics = item.statistics.some(s => { + const hasInvalidStatistics = item.statistics.some(s => { return s.indexOf('p') === 0 && !/p\d{2}\.\d{2}/.test(s); }); if (hasInvalidStatistics) { @@ -402,7 +402,7 @@ export default class CloudWatchDatasource { value: v, }; }); - let useSelectedVariables = + const useSelectedVariables = selectedVariables.some(s => { return s.value === currentVariables[0].value; }) || currentVariables[0].value === '$__all'; diff --git a/public/app/plugins/datasource/cloudwatch/specs/datasource.test.ts b/public/app/plugins/datasource/cloudwatch/specs/datasource.test.ts index a8968008661..eae3e91d37d 100644 --- a/public/app/plugins/datasource/cloudwatch/specs/datasource.test.ts +++ b/public/app/plugins/datasource/cloudwatch/specs/datasource.test.ts @@ -4,18 +4,18 @@ import * as dateMath from 'app/core/utils/datemath'; import _ from 'lodash'; describe('CloudWatchDatasource', function() { - let instanceSettings = { + const instanceSettings = { jsonData: { defaultRegion: 'us-east-1', access: 'proxy' }, }; - let templateSrv = { + const templateSrv = { data: {}, templateSettings: { interpolate: /\[\[([\s\S]+?)\]\]/g }, replace: text => _.template(text, templateSrv.templateSettings)(templateSrv.data), variableExists: () => false, }; - let timeSrv = { + const timeSrv = { time: { from: 'now-1h', to: 'now' }, timeRange: () => { return { @@ -24,8 +24,8 @@ describe('CloudWatchDatasource', function() { }; }, }; - let backendSrv = {}; - let ctx = { + const backendSrv = {}; + const ctx = { backendSrv, templateSrv, }; @@ -121,7 +121,7 @@ describe('CloudWatchDatasource', function() { }); }); - it('should cancel query for invalid extended statistics', function () { + it('should cancel query for invalid extended statistics', function() { var query = { range: { from: 'now-1h', to: 'now' }, rangeRaw: { from: 1483228800, to: 1483232400 }, @@ -252,7 +252,7 @@ describe('CloudWatchDatasource', function() { function describeMetricFindQuery(query, func) { describe('metricFindQuery ' + query, () => { - let scenario: any = {}; + const scenario: any = {}; scenario.setup = setupCallback => { beforeEach(() => { setupCallback(); @@ -461,12 +461,12 @@ describe('CloudWatchDatasource', function() { 3600, ], ]; - for (let t of testData) { - let target = t[0]; - let options = t[1]; - let now = new Date(options.range.from.valueOf() + t[2] * 1000); - let expected = t[3]; - let actual = ctx.ds.getPeriod(target, options, now); + for (const t of testData) { + const target = t[0]; + const options = t[1]; + const now = new Date(options.range.from.valueOf() + t[2] * 1000); + const expected = t[3]; + const actual = ctx.ds.getPeriod(target, options, now); expect(actual).toBe(expected); } }); diff --git a/public/app/plugins/datasource/elasticsearch/datasource.ts b/public/app/plugins/datasource/elasticsearch/datasource.ts index 5a8e83a16cb..b77ebe6b738 100644 --- a/public/app/plugins/datasource/elasticsearch/datasource.ts +++ b/public/app/plugins/datasource/elasticsearch/datasource.ts @@ -414,13 +414,13 @@ export class ElasticDatasource { return true; } - for (let bucketAgg of target.bucketAggs) { + for (const bucketAgg of target.bucketAggs) { if (this.templateSrv.variableExists(bucketAgg.field) || this.objectContainsTemplate(bucketAgg.settings)) { return true; } } - for (let metric of target.metrics) { + for (const metric of target.metrics) { if ( this.templateSrv.variableExists(metric.field) || this.objectContainsTemplate(metric.settings) || @@ -449,13 +449,13 @@ export class ElasticDatasource { return false; } - for (let key of Object.keys(obj)) { + for (const key of Object.keys(obj)) { if (this.isPrimitive(obj[key])) { if (this.templateSrv.variableExists(obj[key])) { return true; } } else if (Array.isArray(obj[key])) { - for (let item of obj[key]) { + for (const item of obj[key]) { if (this.objectContainsTemplate(item)) { return true; } diff --git a/public/app/plugins/datasource/elasticsearch/elastic_response.ts b/public/app/plugins/datasource/elasticsearch/elastic_response.ts index a378ab8b55f..e792d290d5b 100644 --- a/public/app/plugins/datasource/elasticsearch/elastic_response.ts +++ b/public/app/plugins/datasource/elasticsearch/elastic_response.ts @@ -112,29 +112,29 @@ export class ElasticResponse { processAggregationDocs(esAgg, aggDef, target, table, props) { // add columns if (table.columns.length === 0) { - for (let propKey of _.keys(props)) { + for (const propKey of _.keys(props)) { table.addColumn({ text: propKey, filterable: true }); } table.addColumn({ text: aggDef.field, filterable: true }); } // helper func to add values to value array - let addMetricValue = (values, metricName, value) => { + const addMetricValue = (values, metricName, value) => { table.addColumn({ text: metricName }); values.push(value); }; - for (let bucket of esAgg.buckets) { - let values = []; + for (const bucket of esAgg.buckets) { + const values = []; - for (let propValues of _.values(props)) { + for (const propValues of _.values(props)) { values.push(propValues); } // add bucket key (value) values.push(bucket.key); - for (let metric of target.metrics) { + for (const metric of target.metrics) { switch (metric.type) { case 'count': { addMetricValue(values, this.getMetricName(metric.type), bucket.doc_count); @@ -157,7 +157,7 @@ export class ElasticResponse { } default: { let metricName = this.getMetricName(metric.type); - let otherMetrics = _.filter(target.metrics, { type: metric.type }); + const otherMetrics = _.filter(target.metrics, { type: metric.type }); // if more of the same metric type include field field name in property if (otherMetrics.length > 1) { diff --git a/public/app/plugins/datasource/elasticsearch/specs/datasource.test.ts b/public/app/plugins/datasource/elasticsearch/specs/datasource.test.ts index 36e7a63a005..d1e2e3ba835 100644 --- a/public/app/plugins/datasource/elasticsearch/specs/datasource.test.ts +++ b/public/app/plugins/datasource/elasticsearch/specs/datasource.test.ts @@ -6,21 +6,21 @@ import { ElasticDatasource } from '../datasource'; import * as dateMath from 'app/core/utils/datemath'; describe('ElasticDatasource', function() { - let backendSrv = { + const backendSrv = { datasourceRequest: jest.fn(), }; - let $rootScope = { + const $rootScope = { $on: jest.fn(), appEvent: jest.fn(), }; - let templateSrv = { + const templateSrv = { replace: jest.fn(text => text), getAdhocFilters: jest.fn(() => []), }; - let timeSrv = { + const timeSrv = { time: { from: 'now-1h', to: 'now' }, timeRange: jest.fn(() => { return { @@ -33,7 +33,7 @@ describe('ElasticDatasource', function() { }), }; - let ctx = { + const ctx = { $rootScope, backendSrv, }; diff --git a/public/app/plugins/datasource/grafana/datasource.ts b/public/app/plugins/datasource/grafana/datasource.ts index 9fa32fa6503..c3687161414 100644 --- a/public/app/plugins/datasource/grafana/datasource.ts +++ b/public/app/plugins/datasource/grafana/datasource.ts @@ -17,7 +17,7 @@ class GrafanaDatasource { if (res.results) { _.forEach(res.results, queryRes => { - for (let series of queryRes.series) { + for (const series of queryRes.series) { data.push({ target: series.name, datapoints: series.points, diff --git a/public/app/plugins/datasource/graphite/datasource.ts b/public/app/plugins/datasource/graphite/datasource.ts index bc1c5722c3f..d4bdabd1f56 100644 --- a/public/app/plugins/datasource/graphite/datasource.ts +++ b/public/app/plugins/datasource/graphite/datasource.ts @@ -210,8 +210,8 @@ export function GraphiteDatasource(instanceSettings, $q, backendSrv, templateSrv }; this.metricFindQuery = function(query, optionalOptions) { - let options = optionalOptions || {}; - let interpolatedQuery = templateSrv.replace(query); + const options = optionalOptions || {}; + const interpolatedQuery = templateSrv.replace(query); // special handling for tag_values([,]*), this is used for template variables let matches = interpolatedQuery.match(/^tag_values\(([^,]+)((, *[^,]+)*)\)$/); @@ -242,7 +242,7 @@ export function GraphiteDatasource(instanceSettings, $q, backendSrv, templateSrv return this.getTagsAutoComplete(expressions, undefined, options); } - let httpOptions: any = { + const httpOptions: any = { method: 'GET', url: '/metrics/find', params: { @@ -268,9 +268,9 @@ export function GraphiteDatasource(instanceSettings, $q, backendSrv, templateSrv }; this.getTags = function(optionalOptions) { - let options = optionalOptions || {}; + const options = optionalOptions || {}; - let httpOptions: any = { + const httpOptions: any = { method: 'GET', url: '/tags', // for cancellations @@ -293,9 +293,9 @@ export function GraphiteDatasource(instanceSettings, $q, backendSrv, templateSrv }; this.getTagValues = function(tag, optionalOptions) { - let options = optionalOptions || {}; + const options = optionalOptions || {}; - let httpOptions: any = { + const httpOptions: any = { method: 'GET', url: '/tags/' + templateSrv.replace(tag), // for cancellations @@ -322,9 +322,9 @@ export function GraphiteDatasource(instanceSettings, $q, backendSrv, templateSrv }; this.getTagsAutoComplete = (expressions, tagPrefix, optionalOptions) => { - let options = optionalOptions || {}; + const options = optionalOptions || {}; - let httpOptions: any = { + const httpOptions: any = { method: 'GET', url: '/tags/autoComplete/tags', params: { @@ -357,9 +357,9 @@ export function GraphiteDatasource(instanceSettings, $q, backendSrv, templateSrv }; this.getTagValuesAutoComplete = (expressions, tag, valuePrefix, optionalOptions) => { - let options = optionalOptions || {}; + const options = optionalOptions || {}; - let httpOptions: any = { + const httpOptions: any = { method: 'GET', url: '/tags/autoComplete/values', params: { @@ -393,9 +393,9 @@ export function GraphiteDatasource(instanceSettings, $q, backendSrv, templateSrv }; this.getVersion = function(optionalOptions) { - let options = optionalOptions || {}; + const options = optionalOptions || {}; - let httpOptions = { + const httpOptions = { method: 'GET', url: '/version', requestId: options.requestId, @@ -404,7 +404,7 @@ export function GraphiteDatasource(instanceSettings, $q, backendSrv, templateSrv return this.doGraphiteRequest(httpOptions) .then(results => { if (results.data) { - let semver = new SemVersion(results.data); + const semver = new SemVersion(results.data); return semver.isValid() ? results.data : ''; } return ''; @@ -437,7 +437,7 @@ export function GraphiteDatasource(instanceSettings, $q, backendSrv, templateSrv return this.funcDefsPromise; } - let httpOptions = { + const httpOptions = { method: 'GET', url: '/functions', }; @@ -461,7 +461,7 @@ export function GraphiteDatasource(instanceSettings, $q, backendSrv, templateSrv }; this.testDatasource = function() { - let query = { + const query = { panelId: 3, rangeRaw: { from: 'now-1h', to: 'now' }, targets: [{ target: 'constantLine(100)' }], diff --git a/public/app/plugins/datasource/graphite/graphite_query.ts b/public/app/plugins/datasource/graphite/graphite_query.ts index baa58237708..7563a42f583 100644 --- a/public/app/plugins/datasource/graphite/graphite_query.ts +++ b/public/app/plugins/datasource/graphite/graphite_query.ts @@ -59,11 +59,11 @@ export default class GraphiteQuery { } checkForSeriesByTag() { - let seriesByTagFunc = _.find(this.functions, func => func.def.name === 'seriesByTag'); + const seriesByTagFunc = _.find(this.functions, func => func.def.name === 'seriesByTag'); if (seriesByTagFunc) { this.seriesByTagUsed = true; seriesByTagFunc.hidden = true; - let tags = this.splitSeriesByTagParams(seriesByTagFunc); + const tags = this.splitSeriesByTagParams(seriesByTagFunc); this.tags = tags; } } @@ -186,8 +186,8 @@ export default class GraphiteQuery { let refCount = 0; _.each(targetsByRefId, (t, id) => { if (id !== refId) { - let match = nestedSeriesRefRegex.exec(t.target); - let count = match && match.length ? match.length - 1 : 0; + const match = nestedSeriesRefRegex.exec(t.target); + const count = match && match.length ? match.length - 1 : 0; refCount += count; } }); @@ -232,9 +232,9 @@ export default class GraphiteQuery { const tagPattern = /([^\!=~]+)(\!?=~?)(.*)/; return _.flatten( _.map(func.params, (param: string) => { - let matches = tagPattern.exec(param); + const matches = tagPattern.exec(param); if (matches) { - let tag = matches.slice(1); + const tag = matches.slice(1); if (tag.length === 3) { return { key: tag[0], @@ -253,7 +253,7 @@ export default class GraphiteQuery { } getSeriesByTagFunc() { - let seriesByTagFuncIndex = this.getSeriesByTagFuncIndex(); + const seriesByTagFuncIndex = this.getSeriesByTagFuncIndex(); if (seriesByTagFuncIndex >= 0) { return this.functions[seriesByTagFuncIndex]; } else { @@ -262,7 +262,7 @@ export default class GraphiteQuery { } addTag(tag) { - let newTagParam = renderTagString(tag); + const newTagParam = renderTagString(tag); this.getSeriesByTagFunc().params.push(newTagParam); this.tags.push(tag); } @@ -280,7 +280,7 @@ export default class GraphiteQuery { return; } - let newTagParam = renderTagString(tag); + const newTagParam = renderTagString(tag); this.getSeriesByTagFunc().params[tagIndex] = newTagParam; this.tags[tagIndex] = tag; } diff --git a/public/app/plugins/datasource/graphite/query_ctrl.ts b/public/app/plugins/datasource/graphite/query_ctrl.ts index 0563de61705..f73c21e4cc7 100644 --- a/public/app/plugins/datasource/graphite/query_ctrl.ts +++ b/public/app/plugins/datasource/graphite/query_ctrl.ts @@ -49,7 +49,7 @@ export class GraphiteQueryCtrl extends QueryCtrl { return this.uiSegmentSrv.newSegment(segment); }); - let checkOtherSegmentsIndex = this.queryModel.checkOtherSegmentsIndex || 0; + const checkOtherSegmentsIndex = this.queryModel.checkOtherSegmentsIndex || 0; this.checkOtherSegments(checkOtherSegmentsIndex); if (this.queryModel.seriesByTagUsed) { @@ -195,7 +195,7 @@ export class GraphiteQueryCtrl extends QueryCtrl { } if (segment.type === 'tag') { - let tag = removeTagPrefix(segment.value); + const tag = removeTagPrefix(segment.value); this.pause(); this.addSeriesByTagFunc(tag); return; @@ -273,10 +273,10 @@ export class GraphiteQueryCtrl extends QueryCtrl { } addSeriesByTagFunc(tag) { - let newFunc = this.datasource.createFuncInstance('seriesByTag', { + const newFunc = this.datasource.createFuncInstance('seriesByTag', { withDefaultParams: false, }); - let tagParam = `${tag}=`; + const tagParam = `${tag}=`; newFunc.params = [tagParam]; this.queryModel.addFunction(newFunc); newFunc.added = true; @@ -303,23 +303,23 @@ export class GraphiteQueryCtrl extends QueryCtrl { getAllTags() { return this.datasource.getTags().then(values => { - let altTags = _.map(values, 'text'); + const altTags = _.map(values, 'text'); altTags.splice(0, 0, this.removeTagValue); return mapToDropdownOptions(altTags); }); } getTags(index, tagPrefix) { - let tagExpressions = this.queryModel.renderTagExpressions(index); + const tagExpressions = this.queryModel.renderTagExpressions(index); return this.datasource.getTagsAutoComplete(tagExpressions, tagPrefix).then(values => { - let altTags = _.map(values, 'text'); + const altTags = _.map(values, 'text'); altTags.splice(0, 0, this.removeTagValue); return mapToDropdownOptions(altTags); }); } getTagsAsSegments(tagPrefix) { - let tagExpressions = this.queryModel.renderTagExpressions(); + const tagExpressions = this.queryModel.renderTagExpressions(); return this.datasource.getTagsAutoComplete(tagExpressions, tagPrefix).then(values => { return _.map(values, val => { return this.uiSegmentSrv.newSegment({ @@ -336,18 +336,18 @@ export class GraphiteQueryCtrl extends QueryCtrl { } getAllTagValues(tag) { - let tagKey = tag.key; + const tagKey = tag.key; return this.datasource.getTagValues(tagKey).then(values => { - let altValues = _.map(values, 'text'); + const altValues = _.map(values, 'text'); return mapToDropdownOptions(altValues); }); } getTagValues(tag, index, valuePrefix) { - let tagExpressions = this.queryModel.renderTagExpressions(index); - let tagKey = tag.key; + const tagExpressions = this.queryModel.renderTagExpressions(index); + const tagKey = tag.key; return this.datasource.getTagValuesAutoComplete(tagExpressions, tagKey, valuePrefix).then(values => { - let altValues = _.map(values, 'text'); + const altValues = _.map(values, 'text'); // Add template variables as additional values _.eachRight(this.templateSrv.variables, variable => { altValues.push('${' + variable.name + ':regex}'); @@ -362,8 +362,8 @@ export class GraphiteQueryCtrl extends QueryCtrl { } addNewTag(segment) { - let newTagKey = segment.value; - let newTag = { key: newTagKey, operator: '=', value: '' }; + const newTagKey = segment.value; + const newTag = { key: newTagKey, operator: '=', value: '' }; this.queryModel.addTag(newTag); this.targetChanged(); this.fixTagSegments(); diff --git a/public/app/plugins/datasource/graphite/specs/datasource.test.ts b/public/app/plugins/datasource/graphite/specs/datasource.test.ts index f94378c57a6..826f2fed344 100644 --- a/public/app/plugins/datasource/graphite/specs/datasource.test.ts +++ b/public/app/plugins/datasource/graphite/specs/datasource.test.ts @@ -5,7 +5,7 @@ import $q from 'q'; import { TemplateSrvStub } from 'test/specs/helpers'; describe('graphiteDatasource', () => { - let ctx: any = { + const ctx: any = { backendSrv: {}, $q: $q, templateSrv: new TemplateSrvStub(), @@ -18,7 +18,7 @@ describe('graphiteDatasource', () => { }); describe('When querying graphite with one target using query editor target spec', function() { - let query = { + const query = { panelId: 3, dashboardId: 5, rangeRaw: { from: 'now-1h', to: 'now' }, @@ -56,7 +56,7 @@ describe('graphiteDatasource', () => { }); it('should query correctly', function() { - let params = requestOptions.data.split('&'); + const params = requestOptions.data.split('&'); expect(params).toContain('target=prod1.count'); expect(params).toContain('target=prod2.count'); expect(params).toContain('from=-1h'); @@ -64,7 +64,7 @@ describe('graphiteDatasource', () => { }); it('should exclude undefined params', function() { - let params = requestOptions.data.split('&'); + const params = requestOptions.data.split('&'); expect(params).not.toContain('cacheTimeout=undefined'); }); @@ -157,28 +157,28 @@ describe('graphiteDatasource', () => { describe('building graphite params', function() { it('should return empty array if no targets', function() { - let results = ctx.ds.buildGraphiteParams({ + const results = ctx.ds.buildGraphiteParams({ targets: [{}], }); expect(results.length).toBe(0); }); it('should uri escape targets', function() { - let results = ctx.ds.buildGraphiteParams({ + const results = ctx.ds.buildGraphiteParams({ targets: [{ target: 'prod1.{test,test2}' }, { target: 'prod2.count' }], }); expect(results).toContain('target=prod1.%7Btest%2Ctest2%7D'); }); it('should replace target placeholder', function() { - let results = ctx.ds.buildGraphiteParams({ + const results = ctx.ds.buildGraphiteParams({ targets: [{ target: 'series1' }, { target: 'series2' }, { target: 'asPercent(#A,#B)' }], }); expect(results[2]).toBe('target=asPercent(series1%2Cseries2)'); }); it('should replace target placeholder for hidden series', function() { - let results = ctx.ds.buildGraphiteParams({ + const results = ctx.ds.buildGraphiteParams({ targets: [ { target: 'series1', hide: true }, { target: 'sumSeries(#A)', hide: true }, @@ -189,28 +189,28 @@ describe('graphiteDatasource', () => { }); it('should replace target placeholder when nesting query references', function() { - let results = ctx.ds.buildGraphiteParams({ + const results = ctx.ds.buildGraphiteParams({ targets: [{ target: 'series1' }, { target: 'sumSeries(#A)' }, { target: 'asPercent(#A,#B)' }], }); expect(results[2]).toBe('target=' + encodeURIComponent('asPercent(series1,sumSeries(series1))')); }); it('should fix wrong minute interval parameters', function() { - let results = ctx.ds.buildGraphiteParams({ + const results = ctx.ds.buildGraphiteParams({ targets: [{ target: "summarize(prod.25m.count, '25m', 'sum')" }], }); expect(results[0]).toBe('target=' + encodeURIComponent("summarize(prod.25m.count, '25min', 'sum')")); }); it('should fix wrong month interval parameters', function() { - let results = ctx.ds.buildGraphiteParams({ + const results = ctx.ds.buildGraphiteParams({ targets: [{ target: "summarize(prod.5M.count, '5M', 'sum')" }], }); expect(results[0]).toBe('target=' + encodeURIComponent("summarize(prod.5M.count, '5mon', 'sum')")); }); it('should ignore empty targets', function() { - let results = ctx.ds.buildGraphiteParams({ + const results = ctx.ds.buildGraphiteParams({ targets: [{ target: 'series1' }, { target: '' }], }); expect(results.length).toBe(2); @@ -308,19 +308,19 @@ describe('graphiteDatasource', () => { function accessScenario(name, url, fn) { describe('access scenario ' + name, function() { - let ctx: any = { + const ctx: any = { backendSrv: {}, $q: $q, templateSrv: new TemplateSrvStub(), instanceSettings: { url: 'url', name: 'graphiteProd', jsonData: {} }, }; - let httpOptions = { + const httpOptions = { headers: {}, }; describe('when using proxy mode', () => { - let options = { dashboardId: 1, panelId: 2 }; + const options = { dashboardId: 1, panelId: 2 }; it('tracing headers should be added', () => { ctx.instanceSettings.url = url; diff --git a/public/app/plugins/datasource/graphite/specs/graphite_query.test.ts b/public/app/plugins/datasource/graphite/specs/graphite_query.test.ts index d54caae05f8..2169db16c25 100644 --- a/public/app/plugins/datasource/graphite/specs/graphite_query.test.ts +++ b/public/app/plugins/datasource/graphite/specs/graphite_query.test.ts @@ -2,7 +2,7 @@ import gfunc from '../gfunc'; import GraphiteQuery from '../graphite_query'; describe('Graphite query model', () => { - let ctx: any = { + const ctx: any = { datasource: { getFuncDef: gfunc.getFuncDef, getFuncDefs: jest.fn().mockReturnValue(Promise.resolve(gfunc.getFuncDefs('1.0'))), diff --git a/public/app/plugins/datasource/graphite/specs/query_ctrl.test.ts b/public/app/plugins/datasource/graphite/specs/query_ctrl.test.ts index b38ad56427b..7826a458968 100644 --- a/public/app/plugins/datasource/graphite/specs/query_ctrl.test.ts +++ b/public/app/plugins/datasource/graphite/specs/query_ctrl.test.ts @@ -3,7 +3,7 @@ import gfunc from '../gfunc'; import { GraphiteQueryCtrl } from '../query_ctrl'; describe('GraphiteQueryCtrl', () => { - let ctx = { + const ctx = { datasource: { metricFindQuery: jest.fn(() => Promise.resolve([])), getFuncDefs: jest.fn(() => Promise.resolve(gfunc.getFuncDefs('1.0'))), diff --git a/public/app/plugins/datasource/influxdb/datasource.ts b/public/app/plugins/datasource/influxdb/datasource.ts index b9f2b2e03fb..8f5850fa0e8 100644 --- a/public/app/plugins/datasource/influxdb/datasource.ts +++ b/public/app/plugins/datasource/influxdb/datasource.ts @@ -147,15 +147,15 @@ export default class InfluxDatasource { } targetContainsTemplate(target) { - for (let group of target.groupBy) { - for (let param of group.params) { + for (const group of target.groupBy) { + for (const param of group.params) { if (this.templateSrv.variableExists(param)) { return true; } } } - for (let i in target.tags) { + for (const i in target.tags) { if (this.templateSrv.variableExists(target.tags[i].value)) { return true; } @@ -219,7 +219,7 @@ export default class InfluxDatasource { return this._seriesQuery(query) .then(res => { - let error = _.get(res, 'results[0].error'); + const error = _.get(res, 'results[0].error'); if (error) { return { status: 'error', message: error }; } @@ -234,7 +234,7 @@ export default class InfluxDatasource { const currentUrl = this.urls.shift(); this.urls.push(currentUrl); - let params: any = {}; + const params: any = {}; if (this.username) { params.u = this.username; @@ -252,7 +252,7 @@ export default class InfluxDatasource { data = null; } - let req: any = { + const req: any = { method: method, url: currentUrl + url, params: params, diff --git a/public/app/plugins/datasource/influxdb/influx_query.ts b/public/app/plugins/datasource/influxdb/influx_query.ts index 2ef74170068..1ad684699bf 100644 --- a/public/app/plugins/datasource/influxdb/influx_query.ts +++ b/public/app/plugins/datasource/influxdb/influx_query.ts @@ -202,10 +202,10 @@ export default class InfluxQuery { var query = 'SELECT '; var i, y; for (i = 0; i < this.selectModels.length; i++) { - let parts = this.selectModels[i]; + const parts = this.selectModels[i]; var selectText = ''; for (y = 0; y < parts.length; y++) { - let part = parts[y]; + const part = parts[y]; selectText = part.render(selectText); } diff --git a/public/app/plugins/datasource/influxdb/query_ctrl.ts b/public/app/plugins/datasource/influxdb/query_ctrl.ts index 2be1ecc7bff..1b9cd2962fc 100644 --- a/public/app/plugins/datasource/influxdb/query_ctrl.ts +++ b/public/app/plugins/datasource/influxdb/query_ctrl.ts @@ -36,7 +36,7 @@ export class InfluxQueryCtrl extends QueryCtrl { } this.tagSegments = []; - for (let tag of this.target.tags) { + for (const tag of this.target.tags) { if (!tag.operator) { if (/^\/.*\/$/.test(tag.value)) { tag.operator = '=~'; @@ -106,7 +106,7 @@ export class InfluxQueryCtrl extends QueryCtrl { if (!this.queryModel.hasGroupByTime()) { options.push(this.uiSegmentSrv.newSegment({ value: 'time($interval)' })); } - for (let tag of tags) { + for (const tag of tags) { options.push(this.uiSegmentSrv.newSegment({ value: 'tag(' + tag.text + ')' })); } return options; @@ -251,7 +251,7 @@ export class InfluxQueryCtrl extends QueryCtrl { }); if (addTemplateVars) { - for (let variable of this.templateSrv.variables) { + for (const variable of this.templateSrv.variables) { segments.unshift( this.uiSegmentSrv.newSegment({ type: 'value', diff --git a/public/app/plugins/datasource/influxdb/specs/datasource.test.ts b/public/app/plugins/datasource/influxdb/specs/datasource.test.ts index 10974cdad97..60f49bd4905 100644 --- a/public/app/plugins/datasource/influxdb/specs/datasource.test.ts +++ b/public/app/plugins/datasource/influxdb/specs/datasource.test.ts @@ -3,7 +3,7 @@ import $q from 'q'; import { TemplateSrvStub } from 'test/specs/helpers'; describe('InfluxDataSource', () => { - let ctx: any = { + const ctx: any = { backendSrv: {}, $q: $q, templateSrv: new TemplateSrvStub(), @@ -16,8 +16,8 @@ describe('InfluxDataSource', () => { }); describe('When issuing metricFindQuery', () => { - let query = 'SELECT max(value) FROM measurement WHERE $timeFilter'; - let queryOptions: any = { + const query = 'SELECT max(value) FROM measurement WHERE $timeFilter'; + const queryOptions: any = { range: { from: '2018-01-01T00:00:00Z', to: '2018-01-02T00:00:00Z', diff --git a/public/app/plugins/datasource/influxdb/specs/query_ctrl.test.ts b/public/app/plugins/datasource/influxdb/specs/query_ctrl.test.ts index 4e3fc47a5fd..88d4fb143cd 100644 --- a/public/app/plugins/datasource/influxdb/specs/query_ctrl.test.ts +++ b/public/app/plugins/datasource/influxdb/specs/query_ctrl.test.ts @@ -3,7 +3,7 @@ import { uiSegmentSrv } from 'app/core/services/segment_srv'; import { InfluxQueryCtrl } from '../query_ctrl'; describe('InfluxDBQueryCtrl', () => { - let ctx = {}; + const ctx = {}; beforeEach(() => { InfluxQueryCtrl.prototype.datasource = { diff --git a/public/app/plugins/datasource/mssql/query_ctrl.ts b/public/app/plugins/datasource/mssql/query_ctrl.ts index 884eb634f54..1b64a571c6c 100644 --- a/public/app/plugins/datasource/mssql/query_ctrl.ts +++ b/public/app/plugins/datasource/mssql/query_ctrl.ts @@ -59,7 +59,7 @@ export class MssqlQueryCtrl extends QueryCtrl { this.lastQueryMeta = null; this.lastQueryError = null; - let anySeriesFromQuery = _.find(dataList, { refId: this.target.refId }); + const anySeriesFromQuery = _.find(dataList, { refId: this.target.refId }); if (anySeriesFromQuery) { this.lastQueryMeta = anySeriesFromQuery.meta; } @@ -67,7 +67,7 @@ export class MssqlQueryCtrl extends QueryCtrl { onDataError(err) { if (err.data && err.data.results) { - let queryRes = err.data.results[this.target.refId]; + const queryRes = err.data.results[this.target.refId]; if (queryRes) { this.lastQueryMeta = queryRes.meta; this.lastQueryError = queryRes.error; diff --git a/public/app/plugins/datasource/mssql/response_parser.ts b/public/app/plugins/datasource/mssql/response_parser.ts index b6f538707b0..0044a49fd7d 100644 --- a/public/app/plugins/datasource/mssql/response_parser.ts +++ b/public/app/plugins/datasource/mssql/response_parser.ts @@ -10,11 +10,11 @@ export default class ResponseParser { return { data: data }; } - for (let key in res.data.results) { - let queryRes = res.data.results[key]; + for (const key in res.data.results) { + const queryRes = res.data.results[key]; if (queryRes.series) { - for (let series of queryRes.series) { + for (const series of queryRes.series) { data.push({ target: series.name, datapoints: series.points, @@ -25,7 +25,7 @@ export default class ResponseParser { } if (queryRes.tables) { - for (let table of queryRes.tables) { + for (const table of queryRes.tables) { table.type = 'table'; table.refId = queryRes.refId; table.meta = queryRes.meta; diff --git a/public/app/plugins/datasource/mysql/query_ctrl.ts b/public/app/plugins/datasource/mysql/query_ctrl.ts index 4961ce9e653..1de1fb768ad 100644 --- a/public/app/plugins/datasource/mysql/query_ctrl.ts +++ b/public/app/plugins/datasource/mysql/query_ctrl.ts @@ -57,7 +57,7 @@ export class MysqlQueryCtrl extends QueryCtrl { this.lastQueryMeta = null; this.lastQueryError = null; - let anySeriesFromQuery = _.find(dataList, { refId: this.target.refId }); + const anySeriesFromQuery = _.find(dataList, { refId: this.target.refId }); if (anySeriesFromQuery) { this.lastQueryMeta = anySeriesFromQuery.meta; } @@ -65,7 +65,7 @@ export class MysqlQueryCtrl extends QueryCtrl { onDataError(err) { if (err.data && err.data.results) { - let queryRes = err.data.results[this.target.refId]; + const queryRes = err.data.results[this.target.refId]; if (queryRes) { this.lastQueryMeta = queryRes.meta; this.lastQueryError = queryRes.error; diff --git a/public/app/plugins/datasource/mysql/response_parser.ts b/public/app/plugins/datasource/mysql/response_parser.ts index e5d8ab79f2a..339dc592ad2 100644 --- a/public/app/plugins/datasource/mysql/response_parser.ts +++ b/public/app/plugins/datasource/mysql/response_parser.ts @@ -10,11 +10,11 @@ export default class ResponseParser { return { data: data }; } - for (let key in res.data.results) { - let queryRes = res.data.results[key]; + for (const key in res.data.results) { + const queryRes = res.data.results[key]; if (queryRes.series) { - for (let series of queryRes.series) { + for (const series of queryRes.series) { data.push({ target: series.name, datapoints: series.points, @@ -25,7 +25,7 @@ export default class ResponseParser { } if (queryRes.tables) { - for (let table of queryRes.tables) { + for (const table of queryRes.tables) { table.type = 'table'; table.refId = queryRes.refId; table.meta = queryRes.meta; diff --git a/public/app/plugins/datasource/mysql/specs/datasource.test.ts b/public/app/plugins/datasource/mysql/specs/datasource.test.ts index 85fa2b8cc4e..e75ba5e32ee 100644 --- a/public/app/plugins/datasource/mysql/specs/datasource.test.ts +++ b/public/app/plugins/datasource/mysql/specs/datasource.test.ts @@ -3,13 +3,13 @@ import { MysqlDatasource } from '../datasource'; import { CustomVariable } from 'app/features/templating/custom_variable'; describe('MySQLDatasource', function() { - let instanceSettings = { name: 'mysql' }; - let backendSrv = {}; - let templateSrv = { + const instanceSettings = { name: 'mysql' }; + const backendSrv = {}; + const templateSrv = { replace: jest.fn(text => text), }; - let ctx = { + const ctx = { backendSrv, }; diff --git a/public/app/plugins/datasource/opentsdb/specs/datasource.test.ts b/public/app/plugins/datasource/opentsdb/specs/datasource.test.ts index 73eca7cffde..befa39fc80e 100644 --- a/public/app/plugins/datasource/opentsdb/specs/datasource.test.ts +++ b/public/app/plugins/datasource/opentsdb/specs/datasource.test.ts @@ -2,14 +2,14 @@ import OpenTsDatasource from '../datasource'; import $q from 'q'; describe('opentsdb', () => { - let ctx = { + const ctx = { backendSrv: {}, ds: {}, templateSrv: { replace: str => str, }, }; - let instanceSettings = { url: '', jsonData: { tsdbVersion: 1 } }; + const instanceSettings = { url: '', jsonData: { tsdbVersion: 1 } }; beforeEach(() => { ctx.ctrl = new OpenTsDatasource(instanceSettings, $q, ctx.backendSrv, ctx.templateSrv); diff --git a/public/app/plugins/datasource/postgres/query_ctrl.ts b/public/app/plugins/datasource/postgres/query_ctrl.ts index 7afd0cf7253..a9073de22cf 100644 --- a/public/app/plugins/datasource/postgres/query_ctrl.ts +++ b/public/app/plugins/datasource/postgres/query_ctrl.ts @@ -57,7 +57,7 @@ export class PostgresQueryCtrl extends QueryCtrl { this.lastQueryMeta = null; this.lastQueryError = null; - let anySeriesFromQuery = _.find(dataList, { refId: this.target.refId }); + const anySeriesFromQuery = _.find(dataList, { refId: this.target.refId }); if (anySeriesFromQuery) { this.lastQueryMeta = anySeriesFromQuery.meta; } @@ -65,7 +65,7 @@ export class PostgresQueryCtrl extends QueryCtrl { onDataError(err) { if (err.data && err.data.results) { - let queryRes = err.data.results[this.target.refId]; + const queryRes = err.data.results[this.target.refId]; if (queryRes) { this.lastQueryMeta = queryRes.meta; this.lastQueryError = queryRes.error; diff --git a/public/app/plugins/datasource/postgres/response_parser.ts b/public/app/plugins/datasource/postgres/response_parser.ts index ebc9598468b..e7f59e13464 100644 --- a/public/app/plugins/datasource/postgres/response_parser.ts +++ b/public/app/plugins/datasource/postgres/response_parser.ts @@ -10,11 +10,11 @@ export default class ResponseParser { return { data: data }; } - for (let key in res.data.results) { - let queryRes = res.data.results[key]; + for (const key in res.data.results) { + const queryRes = res.data.results[key]; if (queryRes.series) { - for (let series of queryRes.series) { + for (const series of queryRes.series) { data.push({ target: series.name, datapoints: series.points, @@ -25,7 +25,7 @@ export default class ResponseParser { } if (queryRes.tables) { - for (let table of queryRes.tables) { + for (const table of queryRes.tables) { table.type = 'table'; table.refId = queryRes.refId; table.meta = queryRes.meta; @@ -109,7 +109,7 @@ export default class ResponseParser { const table = data.data.results[options.annotation.name].tables[0]; let timeColumnIndex = -1; - let titleColumnIndex = -1; + const titleColumnIndex = -1; let textColumnIndex = -1; let tagsColumnIndex = -1; diff --git a/public/app/plugins/datasource/postgres/specs/datasource.test.ts b/public/app/plugins/datasource/postgres/specs/datasource.test.ts index cd6f57ee3fc..ea150750687 100644 --- a/public/app/plugins/datasource/postgres/specs/datasource.test.ts +++ b/public/app/plugins/datasource/postgres/specs/datasource.test.ts @@ -3,13 +3,13 @@ import { PostgresDatasource } from '../datasource'; import { CustomVariable } from 'app/features/templating/custom_variable'; describe('PostgreSQLDatasource', function() { - let instanceSettings = { name: 'postgresql' }; + const instanceSettings = { name: 'postgresql' }; - let backendSrv = {}; - let templateSrv = { + const backendSrv = {}; + const templateSrv = { replace: jest.fn(text => text), }; - let ctx = { + const ctx = { backendSrv, }; diff --git a/public/app/plugins/datasource/prometheus/completer.ts b/public/app/plugins/datasource/prometheus/completer.ts index 0a974378cde..396a5fc1cd7 100644 --- a/public/app/plugins/datasource/prometheus/completer.ts +++ b/public/app/plugins/datasource/prometheus/completer.ts @@ -24,12 +24,12 @@ export class PromCompleter { } getCompletions(editor, session, pos, prefix, callback) { - let wrappedCallback = (err, completions) => { + const wrappedCallback = (err, completions) => { completions = completions.concat(this.templateVariableCompletions); return callback(err, completions); }; - let token = session.getTokenAt(pos.row, pos.column); + const token = session.getTokenAt(pos.row, pos.column); switch (token.type) { case 'entity.name.tag.label-matcher': @@ -51,8 +51,8 @@ export class PromCompleter { if (token.type === 'paren.lparen' && token.value === '[') { var vectors = []; - for (let unit of ['s', 'm', 'h']) { - for (let value of [1, 5, 10, 30]) { + for (const unit of ['s', 'm', 'h']) { + for (const value of [1, 5, 10, 30]) { vectors.push({ caption: value + unit, value: '[' + value + unit, @@ -99,7 +99,7 @@ export class PromCompleter { } getCompletionsForLabelMatcherName(session, pos) { - let metricName = this.findMetricName(session, pos.row, pos.column); + const metricName = this.findMetricName(session, pos.row, pos.column); if (!metricName) { return Promise.resolve(this.transformToCompletions(['__name__', 'instance', 'job'], 'label name')); } @@ -125,7 +125,7 @@ export class PromCompleter { } getCompletionsForLabelMatcherValue(session, pos) { - let metricName = this.findMetricName(session, pos.row, pos.column); + const metricName = this.findMetricName(session, pos.row, pos.column); if (!metricName) { return Promise.resolve([]); } @@ -163,7 +163,7 @@ export class PromCompleter { } getCompletionsForBinaryOperator(session, pos) { - let keywordOperatorToken = this.findToken(session, pos.row, pos.column, 'keyword.control', null, 'identifier'); + const keywordOperatorToken = this.findToken(session, pos.row, pos.column, 'keyword.control', null, 'identifier'); if (!keywordOperatorToken) { return Promise.resolve([]); } @@ -204,7 +204,7 @@ export class PromCompleter { case 'ignoring': case 'group_left': case 'group_right': - let binaryOperatorToken = this.findToken( + const binaryOperatorToken = this.findToken( session, keywordOperatorToken.row, keywordOperatorToken.column, @@ -243,7 +243,7 @@ export class PromCompleter { return labelNames; }); } else { - let metricName = this.findMetricName(session, binaryOperatorToken.row, binaryOperatorToken.column); + const metricName = this.findMetricName(session, binaryOperatorToken.row, binaryOperatorToken.column); return this.getLabelNameAndValueForExpression(metricName, 'metricName').then(result => { var labelNames = this.transformToCompletions( _.uniq( @@ -332,7 +332,7 @@ export class PromCompleter { // current row c = 0; for (idx = 0; idx < tokens.length; idx++) { - let nc = c + tokens[idx].value.length; + const nc = c + tokens[idx].value.length; if (nc >= column) { break; } diff --git a/public/app/plugins/datasource/prometheus/datasource.ts b/public/app/plugins/datasource/prometheus/datasource.ts index ec214be8554..057bb55b3c3 100644 --- a/public/app/plugins/datasource/prometheus/datasource.ts +++ b/public/app/plugins/datasource/prometheus/datasource.ts @@ -317,7 +317,7 @@ export class PrometheusDatasource { options = _.clone(options); - for (let target of options.targets) { + for (const target of options.targets) { if (!target.expr || target.hide) { continue; } @@ -482,21 +482,21 @@ export class PrometheusDatasource { return this.$q.when([]); } - let scopedVars = { + const scopedVars = { __interval: { text: this.interval, value: this.interval }, __interval_ms: { text: kbn.interval_to_ms(this.interval), value: kbn.interval_to_ms(this.interval) }, ...this.getRangeScopedVars(), }; - let interpolated = this.templateSrv.replace(query, scopedVars, this.interpolateQueryExpr); + const interpolated = this.templateSrv.replace(query, scopedVars, this.interpolateQueryExpr); var metricFindQuery = new PrometheusMetricFindQuery(this, interpolated, this.timeSrv); return metricFindQuery.process(); } getRangeScopedVars() { - let range = this.timeSrv.timeRange(); - let msRange = range.to.diff(range.from); - let sRange = Math.round(msRange / 1000); - let regularRange = kbn.secondsToHms(msRange / 1000); + const range = this.timeSrv.timeRange(); + const msRange = range.to.diff(range.from); + const sRange = Math.round(msRange / 1000); + const regularRange = kbn.secondsToHms(msRange / 1000); return { __range_ms: { text: msRange, value: msRange }, __range_s: { text: sRange, value: sRange }, @@ -537,7 +537,7 @@ export class PrometheusDatasource { }) .value(); - for (let value of series.values) { + for (const value of series.values) { if (value[1] === '1') { var event = { annotation: annotation, @@ -557,7 +557,7 @@ export class PrometheusDatasource { } testDatasource() { - let now = new Date().getTime(); + const now = new Date().getTime(); return this.performInstantQuery({ expr: '1+1' }, now / 1000).then(response => { if (response.data.status === 'success') { return { status: 'success', message: 'Data source is working' }; diff --git a/public/app/plugins/datasource/prometheus/result_transformer.ts b/public/app/plugins/datasource/prometheus/result_transformer.ts index 7cb160e2d8c..1b1420c0b46 100644 --- a/public/app/plugins/datasource/prometheus/result_transformer.ts +++ b/public/app/plugins/datasource/prometheus/result_transformer.ts @@ -5,21 +5,21 @@ export class ResultTransformer { constructor(private templateSrv) {} transform(response: any, options: any): any[] { - let prometheusResult = response.data.data.result; + const prometheusResult = response.data.data.result; if (options.format === 'table') { return [this.transformMetricDataToTable(prometheusResult, options.responseListLength, options.refId)]; } else if (options.format === 'heatmap') { let seriesList = []; prometheusResult.sort(sortSeriesByLabel); - for (let metricData of prometheusResult) { + for (const metricData of prometheusResult) { seriesList.push(this.transformMetricData(metricData, options, options.start, options.end)); } seriesList = this.transformToHistogramOverTime(seriesList); return seriesList; } else { - let seriesList = []; - for (let metricData of prometheusResult) { + const seriesList = []; + for (const metricData of prometheusResult) { if (response.data.data.resultType === 'matrix') { seriesList.push(this.transformMetricData(metricData, options, options.start, options.end)); } else if (response.data.data.resultType === 'vector') { @@ -44,7 +44,7 @@ export class ResultTransformer { throw new Error('Prometheus heatmap error: data should be a time series'); } - for (let value of metricData.values) { + for (const value of metricData.values) { let dp_value = parseFloat(value[1]); if (_.isNaN(dp_value)) { dp_value = null; @@ -96,7 +96,7 @@ export class ResultTransformer { metricLabels[label] = labelIndex + 1; table.columns.push({ text: label, filterable: !label.startsWith('__') }); }); - let valueText = resultCount > 1 ? `Value #${refId}` : 'Value'; + const valueText = resultCount > 1 ? `Value #${refId}` : 'Value'; table.columns.push({ text: valueText }); // Populate rows, set value to empty string when label not present. @@ -175,8 +175,8 @@ export class ResultTransformer { le30 30 10 35 => 10 0 5 */ for (let i = seriesList.length - 1; i > 0; i--) { - let topSeries = seriesList[i].datapoints; - let bottomSeries = seriesList[i - 1].datapoints; + const topSeries = seriesList[i].datapoints; + const bottomSeries = seriesList[i - 1].datapoints; if (!topSeries || !bottomSeries) { throw new Error('Prometheus heatmap transform error: data should be a time series'); } diff --git a/public/app/plugins/datasource/prometheus/specs/completer.test.ts b/public/app/plugins/datasource/prometheus/specs/completer.test.ts index b29e4d27233..59fcc6592fb 100644 --- a/public/app/plugins/datasource/prometheus/specs/completer.test.ts +++ b/public/app/plugins/datasource/prometheus/specs/completer.test.ts @@ -13,10 +13,10 @@ describe('Prometheus editor completer', function() { }; } - let editor = {}; + const editor = {}; - let backendSrv = {}; - let datasourceStub = new PrometheusDatasource({}, {}, backendSrv, {}, {}); + const backendSrv = {}; + const datasourceStub = new PrometheusDatasource({}, {}, backendSrv, {}, {}); datasourceStub.performInstantQuery = jest.fn(() => Promise.resolve({ @@ -36,7 +36,7 @@ describe('Prometheus editor completer', function() { ); datasourceStub.performSuggestQuery = jest.fn(() => Promise.resolve(['node_cpu'])); - let templateSrv = { + const templateSrv = { variables: [ { name: 'var_name', @@ -44,7 +44,7 @@ describe('Prometheus editor completer', function() { }, ], }; - let completer = new PromCompleter(datasourceStub, templateSrv); + const completer = new PromCompleter(datasourceStub, templateSrv); describe('When inside brackets', () => { it('Should return range vectors', () => { diff --git a/public/app/plugins/datasource/prometheus/specs/datasource.test.ts b/public/app/plugins/datasource/prometheus/specs/datasource.test.ts index d52019ac4cc..fd963f7986e 100644 --- a/public/app/plugins/datasource/prometheus/specs/datasource.test.ts +++ b/public/app/plugins/datasource/prometheus/specs/datasource.test.ts @@ -14,8 +14,8 @@ import { jest.mock('../metric_find_query'); describe('PrometheusDatasource', () => { - let ctx: any = {}; - let instanceSettings = { + const ctx: any = {}; + const instanceSettings = { url: 'proxied', directUrl: 'direct', user: 'test', @@ -123,7 +123,7 @@ describe('PrometheusDatasource', () => { ctx.ds.performTimeSeriesQuery = jest.fn().mockReturnValue(responseMock); return ctx.ds.query(ctx.query).then(result => { - let results = result.data; + const results = result.data; return expect(results).toMatchObject(expected); }); }); @@ -153,7 +153,7 @@ describe('PrometheusDatasource', () => { ctx.ds.performTimeSeriesQuery = jest.fn().mockReturnValue(responseMock); return ctx.ds.query(ctx.query).then(result => { - let seriesLabels = _.map(result.data, 'target'); + const seriesLabels = _.map(result.data, 'target'); return expect(seriesLabels).toEqual(expected); }); }); @@ -326,7 +326,7 @@ describe('PrometheusDatasource', () => { describe('metricFindQuery', () => { beforeEach(() => { - let query = 'query_result(topk(5,rate(http_request_duration_microseconds_count[$__interval])))'; + const query = 'query_result(topk(5,rate(http_request_duration_microseconds_count[$__interval])))'; ctx.templateSrvMock.replace = jest.fn(); ctx.timeSrvMock.timeRange = () => { return { @@ -343,17 +343,17 @@ describe('PrometheusDatasource', () => { }); it('should have the correct range and range_ms', () => { - let range = ctx.templateSrvMock.replace.mock.calls[0][1].__range; - let rangeMs = ctx.templateSrvMock.replace.mock.calls[0][1].__range_ms; - let rangeS = ctx.templateSrvMock.replace.mock.calls[0][1].__range_s; + const range = ctx.templateSrvMock.replace.mock.calls[0][1].__range; + const rangeMs = ctx.templateSrvMock.replace.mock.calls[0][1].__range_ms; + const rangeS = ctx.templateSrvMock.replace.mock.calls[0][1].__range_s; expect(range).toEqual({ text: '21s', value: '21s' }); expect(rangeMs).toEqual({ text: 21031, value: 21031 }); expect(rangeS).toEqual({ text: 21, value: 21 }); }); it('should pass the default interval value', () => { - let interval = ctx.templateSrvMock.replace.mock.calls[0][1].__interval; - let intervalMs = ctx.templateSrvMock.replace.mock.calls[0][1].__interval_ms; + const interval = ctx.templateSrvMock.replace.mock.calls[0][1].__interval; + const intervalMs = ctx.templateSrvMock.replace.mock.calls[0][1].__interval_ms; expect(interval).toEqual({ text: '15s', value: '15s' }); expect(intervalMs).toEqual({ text: 15000, value: 15000 }); }); @@ -385,23 +385,23 @@ const HOUR = 60 * MINUTE; const time = ({ hours = 0, seconds = 0, minutes = 0 }) => moment(hours * HOUR + minutes * MINUTE + seconds * SECOND); -let ctx = {}; -let instanceSettings = { +const ctx = {}; +const instanceSettings = { url: 'proxied', directUrl: 'direct', user: 'test', password: 'mupp', jsonData: { httpMethod: 'GET' }, }; -let backendSrv = { +const backendSrv = { datasourceRequest: jest.fn(), }; -let templateSrv = { +const templateSrv = { replace: jest.fn(str => str), }; -let timeSrv = { +const timeSrv = { timeRange: () => { return { to: { diff: () => 2000 }, from: '' }; }, @@ -420,7 +420,7 @@ describe('PrometheusDatasource', () => { 'proxied/api/v1/query_range?query=' + encodeURIComponent('test{job="testjob"}') + '&start=60&end=240&step=60'; beforeEach(async () => { - let response = { + const response = { data: { status: 'success', data: { @@ -443,7 +443,7 @@ describe('PrometheusDatasource', () => { }); it('should generate the correct query', () => { - let res = backendSrv.datasourceRequest.mock.calls[0][0]; + const res = backendSrv.datasourceRequest.mock.calls[0][0]; expect(res.method).toBe('GET'); expect(res.url).toBe(urlExpected); }); @@ -465,7 +465,7 @@ describe('PrometheusDatasource', () => { }; beforeEach(async () => { - let response = { + const response = { status: 'success', data: { data: { @@ -530,7 +530,7 @@ describe('PrometheusDatasource', () => { }; beforeEach(async () => { - let response = { + const response = { status: 'success', data: { data: { @@ -553,7 +553,7 @@ describe('PrometheusDatasource', () => { }); }); it('should generate the correct query', () => { - let res = backendSrv.datasourceRequest.mock.calls[0][0]; + const res = backendSrv.datasourceRequest.mock.calls[0][0]; expect(res.method).toBe('GET'); expect(res.url).toBe(urlExpected); }); @@ -579,7 +579,7 @@ describe('PrometheusDatasource', () => { }; beforeEach(async () => { - let response = { + const response = { status: 'success', data: { data: { @@ -625,7 +625,7 @@ describe('PrometheusDatasource', () => { }; beforeEach(async () => { - let response = { + const response = { status: 'success', data: { data: { @@ -664,7 +664,7 @@ describe('PrometheusDatasource', () => { }; it('should be min interval when greater than auto interval', async () => { - let query = { + const query = { // 6 minute range range: { from: time({ minutes: 1 }), to: time({ minutes: 7 }) }, targets: [ @@ -675,12 +675,12 @@ describe('PrometheusDatasource', () => { ], interval: '5s', }; - let urlExpected = 'proxied/api/v1/query_range?query=test&start=60&end=420&step=10'; + const urlExpected = 'proxied/api/v1/query_range?query=test&start=60&end=420&step=10'; backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response)); ctx.ds = new PrometheusDatasource(instanceSettings, q, backendSrv, templateSrv, timeSrv); await ctx.ds.query(query); - let res = backendSrv.datasourceRequest.mock.calls[0][0]; + const res = backendSrv.datasourceRequest.mock.calls[0][0]; expect(res.method).toBe('GET'); expect(res.url).toBe(urlExpected); }); @@ -696,7 +696,7 @@ describe('PrometheusDatasource', () => { backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response)); ctx.ds = new PrometheusDatasource(instanceSettings, q, backendSrv, templateSrv, timeSrv); await ctx.ds.query(query); - let res = backendSrv.datasourceRequest.mock.calls[0][0]; + const res = backendSrv.datasourceRequest.mock.calls[0][0]; expect(res.method).toBe('GET'); expect(res.url).toBe(urlExpected); }); @@ -717,7 +717,7 @@ describe('PrometheusDatasource', () => { backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response)); ctx.ds = new PrometheusDatasource(instanceSettings, q, backendSrv, templateSrv, timeSrv); await ctx.ds.query(query); - let res = backendSrv.datasourceRequest.mock.calls[0][0]; + const res = backendSrv.datasourceRequest.mock.calls[0][0]; expect(res.method).toBe('GET'); expect(res.url).toBe(urlExpected); }); @@ -734,7 +734,7 @@ describe('PrometheusDatasource', () => { backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response)); ctx.ds = new PrometheusDatasource(instanceSettings, q, backendSrv, templateSrv, timeSrv); await ctx.ds.query(query); - let res = backendSrv.datasourceRequest.mock.calls[0][0]; + const res = backendSrv.datasourceRequest.mock.calls[0][0]; expect(res.method).toBe('GET'); expect(res.url).toBe(urlExpected); }); @@ -756,7 +756,7 @@ describe('PrometheusDatasource', () => { backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response)); ctx.ds = new PrometheusDatasource(instanceSettings, q, backendSrv, templateSrv, timeSrv); await ctx.ds.query(query); - let res = backendSrv.datasourceRequest.mock.calls[0][0]; + const res = backendSrv.datasourceRequest.mock.calls[0][0]; expect(res.method).toBe('GET'); expect(res.url).toBe(urlExpected); }); @@ -777,7 +777,7 @@ describe('PrometheusDatasource', () => { backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response)); ctx.ds = new PrometheusDatasource(instanceSettings, q, backendSrv, templateSrv, timeSrv); await ctx.ds.query(query); - let res = backendSrv.datasourceRequest.mock.calls[0][0]; + const res = backendSrv.datasourceRequest.mock.calls[0][0]; expect(res.method).toBe('GET'); expect(res.url).toBe(urlExpected); }); @@ -799,7 +799,7 @@ describe('PrometheusDatasource', () => { backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response)); ctx.ds = new PrometheusDatasource(instanceSettings, q, backendSrv, templateSrv, timeSrv); await ctx.ds.query(query); - let res = backendSrv.datasourceRequest.mock.calls[0][0]; + const res = backendSrv.datasourceRequest.mock.calls[0][0]; expect(res.method).toBe('GET'); expect(res.url).toBe(urlExpected); }); @@ -821,7 +821,7 @@ describe('PrometheusDatasource', () => { backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response)); ctx.ds = new PrometheusDatasource(instanceSettings, q, backendSrv, templateSrv, timeSrv); await ctx.ds.query(query); - let res = backendSrv.datasourceRequest.mock.calls[0][0]; + const res = backendSrv.datasourceRequest.mock.calls[0][0]; expect(res.method).toBe('GET'); expect(res.url).toBe(urlExpected); }); @@ -843,7 +843,7 @@ describe('PrometheusDatasource', () => { backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response)); ctx.ds = new PrometheusDatasource(instanceSettings, q, backendSrv, templateSrv, timeSrv); await ctx.ds.query(query); - let res = backendSrv.datasourceRequest.mock.calls[0][0]; + const res = backendSrv.datasourceRequest.mock.calls[0][0]; expect(res.method).toBe('GET'); expect(res.url).toBe(urlExpected); }); @@ -886,7 +886,7 @@ describe('PrometheusDatasource', () => { backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response)); ctx.ds = new PrometheusDatasource(instanceSettings, q, backendSrv, templateSrv, timeSrv); await ctx.ds.query(query); - let res = backendSrv.datasourceRequest.mock.calls[0][0]; + const res = backendSrv.datasourceRequest.mock.calls[0][0]; expect(res.method).toBe('GET'); expect(res.url).toBe(urlExpected); @@ -925,7 +925,7 @@ describe('PrometheusDatasource', () => { templateSrv.replace = jest.fn(str => str); ctx.ds = new PrometheusDatasource(instanceSettings, q, backendSrv, templateSrv, timeSrv); await ctx.ds.query(query); - let res = backendSrv.datasourceRequest.mock.calls[0][0]; + const res = backendSrv.datasourceRequest.mock.calls[0][0]; expect(res.method).toBe('GET'); expect(res.url).toBe(urlExpected); @@ -965,7 +965,7 @@ describe('PrometheusDatasource', () => { templateSrv.replace = jest.fn(str => str); ctx.ds = new PrometheusDatasource(instanceSettings, q, backendSrv, templateSrv, timeSrv); await ctx.ds.query(query); - let res = backendSrv.datasourceRequest.mock.calls[0][0]; + const res = backendSrv.datasourceRequest.mock.calls[0][0]; expect(res.method).toBe('GET'); expect(res.url).toBe(urlExpected); @@ -1011,7 +1011,7 @@ describe('PrometheusDatasource', () => { backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response)); ctx.ds = new PrometheusDatasource(instanceSettings, q, backendSrv, templateSrv, timeSrv); await ctx.ds.query(query); - let res = backendSrv.datasourceRequest.mock.calls[0][0]; + const res = backendSrv.datasourceRequest.mock.calls[0][0]; expect(res.method).toBe('GET'); expect(res.url).toBe(urlExpected); @@ -1051,7 +1051,7 @@ describe('PrometheusDatasource', () => { backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response)); ctx.ds = new PrometheusDatasource(instanceSettings, q, backendSrv, templateSrv, timeSrv); await ctx.ds.query(query); - let res = backendSrv.datasourceRequest.mock.calls[0][0]; + const res = backendSrv.datasourceRequest.mock.calls[0][0]; expect(res.method).toBe('GET'); expect(res.url).toBe(urlExpected); @@ -1096,7 +1096,7 @@ describe('PrometheusDatasource', () => { templateSrv.replace = jest.fn(str => str); ctx.ds = new PrometheusDatasource(instanceSettings, q, backendSrv, templateSrv, timeSrv); await ctx.ds.query(query); - let res = backendSrv.datasourceRequest.mock.calls[0][0]; + const res = backendSrv.datasourceRequest.mock.calls[0][0]; expect(res.method).toBe('GET'); expect(res.url).toBe(urlExpected); @@ -1116,7 +1116,7 @@ describe('PrometheusDatasource', () => { describe('PrometheusDatasource for POST', () => { // var ctx = new helpers.ServiceTestContext(); - let instanceSettings = { + const instanceSettings = { url: 'proxied', directUrl: 'direct', user: 'test', @@ -1140,7 +1140,7 @@ describe('PrometheusDatasource for POST', () => { }; beforeEach(async () => { - let response = { + const response = { status: 'success', data: { data: { @@ -1161,7 +1161,7 @@ describe('PrometheusDatasource for POST', () => { }); }); it('should generate the correct query', () => { - let res = backendSrv.datasourceRequest.mock.calls[0][0]; + const res = backendSrv.datasourceRequest.mock.calls[0][0]; expect(res.method).toBe('POST'); expect(res.url).toBe(urlExpected); expect(res.data).toEqual(dataExpected); diff --git a/public/app/plugins/datasource/prometheus/specs/metric_find_query.test.ts b/public/app/plugins/datasource/prometheus/specs/metric_find_query.test.ts index 88f6830cd31..bfbf241ba06 100644 --- a/public/app/plugins/datasource/prometheus/specs/metric_find_query.test.ts +++ b/public/app/plugins/datasource/prometheus/specs/metric_find_query.test.ts @@ -4,7 +4,7 @@ import PrometheusMetricFindQuery from '../metric_find_query'; import q from 'q'; describe('PrometheusMetricFindQuery', function() { - let instanceSettings = { + const instanceSettings = { url: 'proxied', directUrl: 'direct', user: 'test', @@ -15,7 +15,7 @@ describe('PrometheusMetricFindQuery', function() { from: moment.utc('2018-04-25 10:00'), to: moment.utc('2018-04-25 11:00'), }; - let ctx: any = { + const ctx: any = { backendSrvMock: { datasourceRequest: jest.fn(() => Promise.resolve({})), }, diff --git a/public/app/plugins/datasource/prometheus/specs/result_transformer.test.ts b/public/app/plugins/datasource/prometheus/specs/result_transformer.test.ts index 68224121414..ac85e1374bb 100644 --- a/public/app/plugins/datasource/prometheus/specs/result_transformer.test.ts +++ b/public/app/plugins/datasource/prometheus/specs/result_transformer.test.ts @@ -1,7 +1,7 @@ import { ResultTransformer } from '../result_transformer'; describe('Prometheus Result Transformer', () => { - let ctx: any = {}; + const ctx: any = {}; beforeEach(() => { ctx.templateSrv = { @@ -111,7 +111,7 @@ describe('Prometheus Result Transformer', () => { }; it('should convert cumulative histogram to regular', () => { - let options = { + const options = { format: 'heatmap', start: 1445000010, end: 1445000030, @@ -171,7 +171,7 @@ describe('Prometheus Result Transformer', () => { ], }, }; - let options = { + const options = { format: 'timeseries', start: 0, end: 2, @@ -194,7 +194,7 @@ describe('Prometheus Result Transformer', () => { ], }, }; - let options = { + const options = { format: 'timeseries', step: 1, start: 0, @@ -218,7 +218,7 @@ describe('Prometheus Result Transformer', () => { ], }, }; - let options = { + const options = { format: 'timeseries', step: 2, start: 0, diff --git a/public/app/plugins/datasource/testdata/datasource.ts b/public/app/plugins/datasource/testdata/datasource.ts index 327abb1d70b..3f4035830ed 100644 --- a/public/app/plugins/datasource/testdata/datasource.ts +++ b/public/app/plugins/datasource/testdata/datasource.ts @@ -39,7 +39,7 @@ class TestDataDatasource { if (res.results) { _.forEach(res.results, queryRes => { - for (let series of queryRes.series) { + for (const series of queryRes.series) { data.push({ target: series.name, datapoints: series.points, diff --git a/public/app/plugins/panel/alertlist/module.ts b/public/app/plugins/panel/alertlist/module.ts index 55869ce626d..b171f590e94 100644 --- a/public/app/plugins/panel/alertlist/module.ts +++ b/public/app/plugins/panel/alertlist/module.ts @@ -42,7 +42,7 @@ class AlertListPanel extends PanelCtrl { this.events.on('init-edit-mode', this.onInitEditMode.bind(this)); this.events.on('refresh', this.onRefresh.bind(this)); - for (let key in this.panel.stateFilter) { + for (const key in this.panel.stateFilter) { this.stateFilter[this.panel.stateFilter[key]] = true; } } @@ -67,7 +67,7 @@ class AlertListPanel extends PanelCtrl { updateStateFilter() { var result = []; - for (let key in this.stateFilter) { + for (const key in this.stateFilter) { if (this.stateFilter[key]) { result.push(key); } diff --git a/public/app/plugins/panel/graph/data_processor.ts b/public/app/plugins/panel/graph/data_processor.ts index f8162c57a10..0e75399445d 100644 --- a/public/app/plugins/panel/graph/data_processor.ts +++ b/public/app/plugins/panel/graph/data_processor.ts @@ -14,7 +14,7 @@ export class DataProcessor { var firstItem; if (options.dataList && options.dataList.length > 0) { firstItem = options.dataList[0]; - let autoDetectMode = this.getAutoDetectXAxisMode(firstItem); + const autoDetectMode = this.getAutoDetectXAxisMode(firstItem); if (this.panel.xaxis.mode !== autoDetectMode) { this.panel.xaxis.mode = autoDetectMode; this.setPanelDefaultsForNewXAxisMode(); @@ -127,7 +127,7 @@ export class DataProcessor { } customHandler(dataItem) { - let nameField = this.panel.xaxis.name; + const nameField = this.panel.xaxis.name; if (!nameField) { throw { message: 'No field name specified to use for x-axis, check your axes settings', @@ -159,9 +159,9 @@ export class DataProcessor { return []; } - let fields = []; + const fields = []; var firstItem = dataList[0]; - let fieldParts = []; + const fieldParts = []; function getPropertiesRecursive(obj) { _.forEach(obj, (value, key) => { @@ -170,7 +170,7 @@ export class DataProcessor { getPropertiesRecursive(value); } else { if (!onlyNumbers || _.isNumber(value)) { - let field = fieldParts.concat(key).join('.'); + const field = fieldParts.concat(key).join('.'); fields.push(field); } } @@ -205,7 +205,7 @@ export class DataProcessor { } pluckDeep(obj: any, property: string) { - let propertyParts = property.split('.'); + const propertyParts = property.split('.'); let value = obj; for (let i = 0; i < propertyParts.length; ++i) { if (value[propertyParts[i]]) { diff --git a/public/app/plugins/panel/graph/graph.ts b/public/app/plugins/panel/graph/graph.ts index 35886aa5bf7..bfbbc855f20 100755 --- a/public/app/plugins/panel/graph/graph.ts +++ b/public/app/plugins/panel/graph/graph.ts @@ -148,7 +148,7 @@ class GraphElement { if ((pos.ctrlKey || pos.metaKey) && (this.dashboard.meta.canEdit || this.dashboard.meta.canMakeEditable)) { // Skip if range selected (added in "plotselected" event handler) - let isRangeSelection = pos.x !== pos.x1; + const isRangeSelection = pos.x !== pos.x1; if (!isRangeSelection) { setTimeout(() => { this.eventManager.updateTime({ from: pos.x, to: null }); @@ -269,7 +269,7 @@ class GraphElement { this.panel.dashes = this.panel.lines ? this.panel.dashes : false; // Populate element - let options: any = this.buildFlotOptions(this.panel); + const options: any = this.buildFlotOptions(this.panel); this.prepareXAxis(options, this.panel); this.configureYAxisOptions(this.data, options); this.thresholdManager.addFlotOptions(options, this.panel); @@ -281,7 +281,7 @@ class GraphElement { buildFlotPairs(data) { for (let i = 0; i < data.length; i++) { - let series = data[i]; + const series = data[i]; series.data = series.getFlotPairs(series.nullPointMode || this.panel.nullPointMode); // if hidden remove points and disable stack @@ -299,7 +299,7 @@ class GraphElement { options.series.bars.align = 'center'; for (let i = 0; i < this.data.length; i++) { - let series = this.data[i]; + const series = this.data[i]; series.data = [[i + 1, series.stats[panel.xaxis.values[0]]]]; } @@ -310,9 +310,9 @@ class GraphElement { let bucketSize: number; if (this.data.length) { - let histMin = _.min(_.map(this.data, s => s.stats.min)); - let histMax = _.max(_.map(this.data, s => s.stats.max)); - let ticks = panel.xaxis.buckets || this.panelWidth / 50; + const histMin = _.min(_.map(this.data, s => s.stats.min)); + const histMax = _.max(_.map(this.data, s => s.stats.max)); + const ticks = panel.xaxis.buckets || this.panelWidth / 50; bucketSize = tickStep(histMin, histMax, ticks); options.series.bars.barWidth = bucketSize * 0.8; this.data = convertToHistogramData(this.data, bucketSize, this.ctrl.hiddenSeries, histMin, histMax); @@ -362,7 +362,7 @@ class GraphElement { gridColor = '#a1a1a1'; } const stack = panel.stack ? true : null; - let options = { + const options = { hooks: { draw: [this.drawHook.bind(this)], processOffset: [this.processOffsetHook.bind(this)], @@ -481,12 +481,12 @@ class GraphElement { addXHistogramAxis(options, bucketSize) { let ticks, min, max; - let defaultTicks = this.panelWidth / 50; + const defaultTicks = this.panelWidth / 50; if (this.data.length && bucketSize) { - let tick_values = []; - for (let d of this.data) { - for (let point of d.data) { + const tick_values = []; + for (const d of this.data) { + for (const point of d.data) { tick_values[point[0]] = true; } } diff --git a/public/app/plugins/panel/graph/graph_tooltip.ts b/public/app/plugins/panel/graph/graph_tooltip.ts index 7bbafc453eb..da2d25b1366 100644 --- a/public/app/plugins/panel/graph/graph_tooltip.ts +++ b/public/app/plugins/panel/graph/graph_tooltip.ts @@ -2,20 +2,20 @@ import $ from 'jquery'; import { appEvents } from 'app/core/core'; export default function GraphTooltip(elem, dashboard, scope, getSeriesFn) { - let self = this; - let ctrl = scope.ctrl; - let panel = ctrl.panel; + const self = this; + const ctrl = scope.ctrl; + const panel = ctrl.panel; - let $tooltip = $('
    '); + const $tooltip = $('
    '); this.destroy = function() { $tooltip.remove(); }; this.findHoverIndexFromDataPoints = function(posX, series, last) { - let ps = series.datapoints.pointsize; - let initial = last * ps; - let len = series.datapoints.points.length; + const ps = series.datapoints.pointsize; + const initial = last * ps; + const len = series.datapoints.points.length; let j; for (j = initial; j < len; j += ps) { // Special case of a non stepped line, highlight the very last point just before a null point @@ -149,7 +149,7 @@ export default function GraphTooltip(elem, dashboard, scope, getSeriesFn) { elem.mouseleave(function() { if (panel.tooltip.shared) { - let plot = elem.data().plot; + const plot = elem.data().plot; if (plot) { $tooltip.detach(); plot.unhighlight(); @@ -177,25 +177,25 @@ export default function GraphTooltip(elem, dashboard, scope, getSeriesFn) { }; this.show = function(pos, item) { - let plot = elem.data().plot; - let plotData = plot.getData(); - let xAxes = plot.getXAxes(); - let xMode = xAxes[0].options.mode; - let seriesList = getSeriesFn(); + const plot = elem.data().plot; + const plotData = plot.getData(); + const xAxes = plot.getXAxes(); + const xMode = xAxes[0].options.mode; + const seriesList = getSeriesFn(); let allSeriesMode = panel.tooltip.shared; let group, value, absoluteTime, hoverInfo, i, series, seriesHtml, tooltipFormat; // if panelRelY is defined another panel wants us to show a tooltip // get pageX from position on x axis and pageY from relative position in original panel if (pos.panelRelY) { - let pointOffset = plot.pointOffset({ x: pos.x }); + const pointOffset = plot.pointOffset({ x: pos.x }); if (Number.isNaN(pointOffset.left) || pointOffset.left < 0 || pointOffset.left > elem.width()) { self.clear(plot); return; } pos.pageX = elem.offset().left + pointOffset.left; pos.pageY = elem.offset().top + elem.height() * pos.panelRelY; - let isVisible = + const isVisible = pos.pageY >= $(window).scrollTop() && pos.pageY <= $(window).innerHeight() + $(window).scrollTop(); if (!isVisible) { self.clear(plot); @@ -223,7 +223,7 @@ export default function GraphTooltip(elem, dashboard, scope, getSeriesFn) { if (allSeriesMode) { plot.unhighlight(); - let seriesHoverInfo = self.getMultiSeriesPlotHoverInfo(plotData, pos); + const seriesHoverInfo = self.getMultiSeriesPlotHoverInfo(plotData, pos); seriesHtml = ''; diff --git a/public/app/plugins/panel/graph/histogram.ts b/public/app/plugins/panel/graph/histogram.ts index ad56e477a85..f8819041cba 100644 --- a/public/app/plugins/panel/graph/histogram.ts +++ b/public/app/plugins/panel/graph/histogram.ts @@ -7,12 +7,12 @@ import TimeSeries from 'app/core/time_series2'; */ export function getSeriesValues(dataList: TimeSeries[]): number[] { const VALUE_INDEX = 0; - let values = []; + const values = []; // Count histogam stats for (let i = 0; i < dataList.length; i++) { - let series = dataList[i]; - let datapoints = series.datapoints; + const series = dataList[i]; + const datapoints = series.datapoints; for (let j = 0; j < datapoints.length; j++) { if (datapoints[j][VALUE_INDEX] !== null) { values.push(datapoints[j][VALUE_INDEX]); @@ -30,10 +30,10 @@ export function getSeriesValues(dataList: TimeSeries[]): number[] { * @param bucketSize */ export function convertValuesToHistogram(values: number[], bucketSize: number, min: number, max: number): any[] { - let histogram = {}; + const histogram = {}; - let minBound = getBucketBound(min, bucketSize); - let maxBound = getBucketBound(max, bucketSize); + const minBound = getBucketBound(min, bucketSize); + const maxBound = getBucketBound(max, bucketSize); let bound = minBound; let n = 0; while (bound <= maxBound) { @@ -43,11 +43,11 @@ export function convertValuesToHistogram(values: number[], bucketSize: number, m } for (let i = 0; i < values.length; i++) { - let bound = getBucketBound(values[i], bucketSize); + const bound = getBucketBound(values[i], bucketSize); histogram[bound] = histogram[bound] + 1; } - let histogam_series = _.map(histogram, (count, bound) => { + const histogam_series = _.map(histogram, (count, bound) => { return [Number(bound), count]; }); @@ -68,10 +68,10 @@ export function convertToHistogramData( max: number ): any[] { return data.map(series => { - let values = getSeriesValues([series]); + const values = getSeriesValues([series]); series.histogram = true; if (!hiddenSeries[series.alias]) { - let histogram = convertValuesToHistogram(values, bucketSize, min, max); + const histogram = convertValuesToHistogram(values, bucketSize, min, max); series.data = histogram; } else { series.data = []; diff --git a/public/app/plugins/panel/graph/jquery.flot.events.ts b/public/app/plugins/panel/graph/jquery.flot.events.ts index 9dfe0a8573f..2f05a76d02b 100644 --- a/public/app/plugins/panel/graph/jquery.flot.events.ts +++ b/public/app/plugins/panel/graph/jquery.flot.events.ts @@ -5,16 +5,16 @@ import Drop from 'tether-drop'; /** @ngInject */ export function createAnnotationToolip(element, event, plot) { - let injector = angular.element(document).injector(); - let content = document.createElement('div'); + const injector = angular.element(document).injector(); + const content = document.createElement('div'); content.innerHTML = ''; injector.invoke([ '$compile', '$rootScope', function($compile, $rootScope) { - let eventManager = plot.getOptions().events.manager; - let tmpScope = $rootScope.$new(true); + const eventManager = plot.getOptions().events.manager; + const tmpScope = $rootScope.$new(true); tmpScope.event = event; tmpScope.onEdit = function() { eventManager.editEvent(event); @@ -24,7 +24,7 @@ export function createAnnotationToolip(element, event, plot) { tmpScope.$digest(); tmpScope.$destroy(); - let drop = new Drop({ + const drop = new Drop({ target: element[0], content: content, position: 'bottom center', @@ -51,7 +51,7 @@ let markerElementToAttachTo = null; /** @ngInject */ export function createEditPopover(element, event, plot) { - let eventManager = plot.getOptions().events.manager; + const eventManager = plot.getOptions().events.manager; if (eventManager.editorOpen) { // update marker element to attach to (needed in case of legend on the right // when there is a double render pass and the inital marker element is removed) @@ -66,15 +66,15 @@ export function createEditPopover(element, event, plot) { // wait for element to be attached and positioned setTimeout(function() { - let injector = angular.element(document).injector(); - let content = document.createElement('div'); + const injector = angular.element(document).injector(); + const content = document.createElement('div'); content.innerHTML = ''; injector.invoke([ '$compile', '$rootScope', function($compile, $rootScope) { - let scope = $rootScope.$new(true); + const scope = $rootScope.$new(true); let drop; scope.event = event; @@ -240,22 +240,22 @@ export class EventMarkers { * create internal objects for the given events */ setupEvents(events) { - let parts = _.partition(events, 'isRegion'); - let regions = parts[0]; + const parts = _.partition(events, 'isRegion'); + const regions = parts[0]; events = parts[1]; $.each(events, (index, event) => { - let ve = new VisualEvent(event, this._buildDiv(event)); + const ve = new VisualEvent(event, this._buildDiv(event)); this._events.push(ve); }); $.each(regions, (index, event) => { - let vre = new VisualEvent(event, this._buildRegDiv(event)); + const vre = new VisualEvent(event, this._buildRegDiv(event)); this._events.push(vre); }); this._events.sort((a, b) => { - let ao = a.getOptions(), + const ao = a.getOptions(), bo = b.getOptions(); if (ao.min > bo.min) { return 1; @@ -293,7 +293,7 @@ export class EventMarkers { let o = this._plot.getPlotOffset(), left, top; - let xaxis = this._plot.getXAxes()[this._plot.getOptions().events.xaxis - 1]; + const xaxis = this._plot.getXAxes()[this._plot.getOptions().events.xaxis - 1]; $.each(this._events, (index, event) => { top = o.top + this._plot.height() - event.visual().height(); @@ -316,16 +316,16 @@ export class EventMarkers { * create a DOM element for the given event */ _buildDiv(event) { - let that = this; + const that = this; - let container = this._plot.getPlaceholder(); - let o = this._plot.getPlotOffset(); - let xaxis = this._plot.getXAxes()[this._plot.getOptions().events.xaxis - 1]; + const container = this._plot.getPlaceholder(); + const o = this._plot.getPlotOffset(); + const xaxis = this._plot.getXAxes()[this._plot.getOptions().events.xaxis - 1]; let top, left, color, markerSize, markerShow, lineStyle, lineWidth; let markerTooltip; // map the eventType to a types object - let eventTypeId = event.eventType; + const eventTypeId = event.eventType; if (this._types === null || !this._types[eventTypeId] || !this._types[eventTypeId].color) { color = '#666'; @@ -369,7 +369,7 @@ export class EventMarkers { top = o.top + this._plot.height() + topOffset; left = xaxis.p2c(event.min) + o.left; - let line = $('
    ') + const line = $('
    ') .css({ position: 'absolute', opacity: 0.8, @@ -385,7 +385,7 @@ export class EventMarkers { .appendTo(container); if (markerShow) { - let marker = $('
    ').css({ + const marker = $('
    ').css({ position: 'absolute', left: -markerSize - Math.round(lineWidth / 2) + 'px', 'font-size': 0, @@ -420,7 +420,7 @@ export class EventMarkers { event: event, }); - let mouseenter = function() { + const mouseenter = function() { createAnnotationToolip(marker, $(this).data('event'), that._plot); }; @@ -428,7 +428,7 @@ export class EventMarkers { createEditPopover(marker, event.editModel, that._plot); } - let mouseleave = function() { + const mouseleave = function() { that._plot.clearSelection(); }; @@ -438,7 +438,7 @@ export class EventMarkers { } } - let drawableEvent = new DrawableEvent( + const drawableEvent = new DrawableEvent( line, function drawFunc(obj) { obj.show(); @@ -465,15 +465,15 @@ export class EventMarkers { * create a DOM element for the given region */ _buildRegDiv(event) { - let that = this; + const that = this; - let container = this._plot.getPlaceholder(); - let o = this._plot.getPlotOffset(); - let xaxis = this._plot.getXAxes()[this._plot.getOptions().events.xaxis - 1]; + const container = this._plot.getPlaceholder(); + const o = this._plot.getPlotOffset(); + const xaxis = this._plot.getXAxes()[this._plot.getOptions().events.xaxis - 1]; let top, left, lineWidth, regionWidth, lineStyle, color, markerTooltip; // map the eventType to a types object - let eventTypeId = event.eventType; + const eventTypeId = event.eventType; if (this._types === null || !this._types[eventTypeId] || !this._types[eventTypeId].color) { color = '#666'; @@ -499,17 +499,17 @@ export class EventMarkers { lineStyle = this._types[eventTypeId].lineStyle.toLowerCase(); } - let topOffset = 2; + const topOffset = 2; top = o.top + this._plot.height() + topOffset; - let timeFrom = Math.min(event.min, event.timeEnd); - let timeTo = Math.max(event.min, event.timeEnd); + const timeFrom = Math.min(event.min, event.timeEnd); + const timeTo = Math.max(event.min, event.timeEnd); left = xaxis.p2c(timeFrom) + o.left; - let right = xaxis.p2c(timeTo) + o.left; + const right = xaxis.p2c(timeTo) + o.left; regionWidth = right - left; _.each([left, right], position => { - let line = $('
    ').css({ + const line = $('
    ').css({ position: 'absolute', opacity: 0.8, left: position + 'px', @@ -524,7 +524,7 @@ export class EventMarkers { line.appendTo(container); }); - let region = $('
    ').css({ + const region = $('
    ').css({ position: 'absolute', opacity: 0.5, left: left + 'px', @@ -541,7 +541,7 @@ export class EventMarkers { event: event, }); - let mouseenter = function() { + const mouseenter = function() { createAnnotationToolip(region, $(this).data('event'), that._plot); }; @@ -549,7 +549,7 @@ export class EventMarkers { createEditPopover(region, event.editModel, that._plot); } - let mouseleave = function() { + const mouseleave = function() { that._plot.clearSelection(); }; @@ -558,7 +558,7 @@ export class EventMarkers { region.hover(mouseenter, mouseleave); } - let drawableEvent = new DrawableEvent( + const drawableEvent = new DrawableEvent( region, function drawFunc(obj) { obj.show(); @@ -585,8 +585,8 @@ export class EventMarkers { * check if the event is inside visible range */ _insidePlot(x) { - let xaxis = this._plot.getXAxes()[this._plot.getOptions().events.xaxis - 1]; - let xc = xaxis.p2c(x); + const xaxis = this._plot.getXAxes()[this._plot.getOptions().events.xaxis - 1]; + const xc = xaxis.p2c(x); return xc > 0 && xc < xaxis.p2c(xaxis.max); } } @@ -598,8 +598,8 @@ export class EventMarkers { /** @ngInject */ export function init(plot) { /*jshint validthis:true */ - let that = this; - let eventMarkers = new EventMarkers(plot); + const that = this; + const eventMarkers = new EventMarkers(plot); plot.getEvents = function() { return eventMarkers._events; @@ -638,7 +638,7 @@ export function init(plot) { }); plot.hooks.draw.push(function(plot) { - let options = plot.getOptions(); + const options = plot.getOptions(); if (eventMarkers.eventsEnabled) { // check for first run @@ -654,7 +654,7 @@ export function init(plot) { }); } -let defaultOptions = { +const defaultOptions = { events: { data: null, types: null, diff --git a/public/app/plugins/panel/graph/legend.ts b/public/app/plugins/panel/graph/legend.ts index f5c35ad98bf..f735fe28b22 100644 --- a/public/app/plugins/panel/graph/legend.ts +++ b/public/app/plugins/panel/graph/legend.ts @@ -16,7 +16,7 @@ module.directive('graphLegend', function(popoverSrv, $timeout) { var i; var legendScrollbar; const legendRightDefaultWidth = 10; - let legendElem = elem.parent(); + const legendElem = elem.parent(); scope.$on('$destroy', function() { destroyScrollbar(); @@ -111,7 +111,7 @@ module.directive('graphLegend', function(popoverSrv, $timeout) { } function render() { - let legendWidth = legendElem.width(); + const legendWidth = legendElem.width(); if (!ctrl.panel.legend.show) { elem.empty(); firstRender = true; @@ -176,7 +176,7 @@ module.directive('graphLegend', function(popoverSrv, $timeout) { } function renderSeriesLegendElements() { - let seriesElements = []; + const seriesElements = []; for (i = 0; i < seriesList.length; i++) { var series = seriesList[i]; @@ -231,7 +231,7 @@ module.directive('graphLegend', function(popoverSrv, $timeout) { } function renderLegendElement(tableHeaderElem) { - let legendWidth = elem.width(); + const legendWidth = elem.width(); var seriesElements = renderSeriesLegendElements(); @@ -262,8 +262,8 @@ module.directive('graphLegend', function(popoverSrv, $timeout) {
    `; - let scrollRoot = elem; - let scroller = elem.find('.graph-legend-scroll'); + const scrollRoot = elem; + const scroller = elem.find('.graph-legend-scroll'); // clear existing scroll bar track to prevent duplication scrollRoot.find('.baron__track').remove(); @@ -272,7 +272,7 @@ module.directive('graphLegend', function(popoverSrv, $timeout) { $(scrollBarHTML).appendTo(scrollRoot); scroller.addClass(scrollerClass); - let scrollbarParams = { + const scrollbarParams = { root: scrollRoot[0], scroller: scroller[0], bar: '.baron__bar', diff --git a/public/app/plugins/panel/graph/module.ts b/public/app/plugins/panel/graph/module.ts index ba151692147..97999158446 100644 --- a/public/app/plugins/panel/graph/module.ts +++ b/public/app/plugins/panel/graph/module.ts @@ -196,7 +196,7 @@ class GraphCtrl extends MetricsPanelCtrl { tip: 'No datapoints returned from data query', }; } else { - for (let series of this.seriesList) { + for (const series of this.seriesList) { if (series.isOutsideRange) { this.dataWarning = { title: 'Data points outside time range', @@ -226,7 +226,7 @@ class GraphCtrl extends MetricsPanelCtrl { return; } - for (let series of this.seriesList) { + for (const series of this.seriesList) { series.applySeriesOverrides(this.panel.seriesOverrides); if (series.unit) { diff --git a/public/app/plugins/panel/graph/specs/graph.test.ts b/public/app/plugins/panel/graph/specs/graph.test.ts index f75f7cd68ea..2ae76bb9c9c 100644 --- a/public/app/plugins/panel/graph/specs/graph.test.ts +++ b/public/app/plugins/panel/graph/specs/graph.test.ts @@ -28,9 +28,9 @@ import moment from 'moment'; import $ from 'jquery'; import { graphDirective } from '../graph'; -let ctx = {}; +const ctx = {}; let ctrl; -let scope = { +const scope = { ctrl: {}, range: { from: moment([2015, 1, 1]), diff --git a/public/app/plugins/panel/graph/specs/graph_ctrl.test.ts b/public/app/plugins/panel/graph/specs/graph_ctrl.test.ts index a0c7dd0ab9c..49efa8d4120 100644 --- a/public/app/plugins/panel/graph/specs/graph_ctrl.test.ts +++ b/public/app/plugins/panel/graph/specs/graph_ctrl.test.ts @@ -4,7 +4,7 @@ import { GraphCtrl } from '../module'; jest.mock('../graph', () => ({})); describe('GraphCtrl', () => { - let injector = { + const injector = { get: () => { return { timeRange: () => { @@ -17,7 +17,7 @@ describe('GraphCtrl', () => { }, }; - let scope = { + const scope = { $on: () => {}, }; @@ -30,7 +30,7 @@ describe('GraphCtrl', () => { }, }; - let ctx = {}; + const ctx = {}; beforeEach(() => { ctx.ctrl = new GraphCtrl(scope, injector, {}); diff --git a/public/app/plugins/panel/graph/specs/histogram.test.ts b/public/app/plugins/panel/graph/specs/histogram.test.ts index 0e9eaa8b98e..adbc0fcba68 100644 --- a/public/app/plugins/panel/graph/specs/histogram.test.ts +++ b/public/app/plugins/panel/graph/specs/histogram.test.ts @@ -11,17 +11,17 @@ describe('Graph Histogam Converter', function() { it('Should convert to series-like array', () => { bucketSize = 10; - let expected = [[0, 2], [10, 3], [20, 2]]; + const expected = [[0, 2], [10, 3], [20, 2]]; - let histogram = convertValuesToHistogram(values, bucketSize, 1, 29); + const histogram = convertValuesToHistogram(values, bucketSize, 1, 29); expect(histogram).toMatchObject(expected); }); it('Should not add empty buckets', () => { bucketSize = 5; - let expected = [[0, 2], [5, 0], [10, 2], [15, 1], [20, 1], [25, 1]]; + const expected = [[0, 2], [5, 0], [10, 2], [15, 1], [20, 1], [25, 1]]; - let histogram = convertValuesToHistogram(values, bucketSize, 1, 29); + const histogram = convertValuesToHistogram(values, bucketSize, 1, 29); expect(histogram).toMatchObject(expected); }); }); @@ -38,18 +38,18 @@ describe('Graph Histogam Converter', function() { }); it('Should convert to values array', () => { - let expected = [1, 2, 10, 11, 17, 20, 29]; + const expected = [1, 2, 10, 11, 17, 20, 29]; - let values = getSeriesValues(data); + const values = getSeriesValues(data); expect(values).toMatchObject(expected); }); it('Should skip null values', () => { data[0].datapoints.push([null, 0]); - let expected = [1, 2, 10, 11, 17, 20, 29]; + const expected = [1, 2, 10, 11, 17, 20, 29]; - let values = getSeriesValues(data); + const values = getSeriesValues(data); expect(values).toMatchObject(expected); }); }); diff --git a/public/app/plugins/panel/graph/specs/series_override_ctrl.test.ts b/public/app/plugins/panel/graph/specs/series_override_ctrl.test.ts index 2e7456a132a..40b6c1ba561 100644 --- a/public/app/plugins/panel/graph/specs/series_override_ctrl.test.ts +++ b/public/app/plugins/panel/graph/specs/series_override_ctrl.test.ts @@ -2,7 +2,7 @@ import '../series_overrides_ctrl'; import { SeriesOverridesCtrl } from '../series_overrides_ctrl'; describe('SeriesOverridesCtrl', () => { - let popoverSrv = {}; + const popoverSrv = {}; let $scope; beforeEach(() => { diff --git a/public/app/plugins/panel/heatmap/color_legend.ts b/public/app/plugins/panel/heatmap/color_legend.ts index cae1e5fac7f..84ecd2389b6 100644 --- a/public/app/plugins/panel/heatmap/color_legend.ts +++ b/public/app/plugins/panel/heatmap/color_legend.ts @@ -6,7 +6,7 @@ import { contextSrv } from 'app/core/core'; import { tickStep } from 'app/core/utils/ticks'; import { getColorScale, getOpacityScale } from './color_scale'; -let module = angular.module('grafana.directives'); +const module = angular.module('grafana.directives'); const LEGEND_HEIGHT_PX = 6; const LEGEND_WIDTH_PX = 100; @@ -21,8 +21,8 @@ module.directive('colorLegend', function() { restrict: 'E', template: '
    ', link: function(scope, elem, attrs) { - let ctrl = scope.ctrl; - let panel = scope.ctrl.panel; + const ctrl = scope.ctrl; + const panel = scope.ctrl.panel; render(); @@ -31,17 +31,17 @@ module.directive('colorLegend', function() { }); function render() { - let legendElem = $(elem).find('svg'); - let legendWidth = Math.floor(legendElem.outerWidth()); + const legendElem = $(elem).find('svg'); + const legendWidth = Math.floor(legendElem.outerWidth()); if (panel.color.mode === 'spectrum') { - let colorScheme = _.find(ctrl.colorSchemes, { + const colorScheme = _.find(ctrl.colorSchemes, { value: panel.color.colorScheme, }); - let colorScale = getColorScale(colorScheme, contextSrv.user.lightTheme, legendWidth); + const colorScale = getColorScale(colorScheme, contextSrv.user.lightTheme, legendWidth); drawSimpleColorLegend(elem, colorScale); } else if (panel.color.mode === 'opacity') { - let colorOptions = panel.color; + const colorOptions = panel.color; drawSimpleOpacityLegend(elem, colorOptions); } } @@ -57,8 +57,8 @@ module.directive('heatmapLegend', function() { restrict: 'E', template: `
    `, link: function(scope, elem, attrs) { - let ctrl = scope.ctrl; - let panel = scope.ctrl.panel; + const ctrl = scope.ctrl; + const panel = scope.ctrl.panel; render(); ctrl.events.on('render', function() { @@ -68,18 +68,18 @@ module.directive('heatmapLegend', function() { function render() { clearLegend(elem); if (!_.isEmpty(ctrl.data) && !_.isEmpty(ctrl.data.cards)) { - let rangeFrom = 0; - let rangeTo = ctrl.data.cardStats.max; - let maxValue = panel.color.max || rangeTo; - let minValue = panel.color.min || 0; + const rangeFrom = 0; + const rangeTo = ctrl.data.cardStats.max; + const maxValue = panel.color.max || rangeTo; + const minValue = panel.color.min || 0; if (panel.color.mode === 'spectrum') { - let colorScheme = _.find(ctrl.colorSchemes, { + const colorScheme = _.find(ctrl.colorSchemes, { value: panel.color.colorScheme, }); drawColorLegend(elem, colorScheme, rangeFrom, rangeTo, maxValue, minValue); } else if (panel.color.mode === 'opacity') { - let colorOptions = panel.color; + const colorOptions = panel.color; drawOpacityLegend(elem, colorOptions, rangeFrom, rangeTo, maxValue, minValue); } } @@ -89,21 +89,21 @@ module.directive('heatmapLegend', function() { }); function drawColorLegend(elem, colorScheme, rangeFrom, rangeTo, maxValue, minValue) { - let legendElem = $(elem).find('svg'); - let legend = d3.select(legendElem.get(0)); + const legendElem = $(elem).find('svg'); + const legend = d3.select(legendElem.get(0)); clearLegend(elem); - let legendWidth = Math.floor(legendElem.outerWidth()) - 30; - let legendHeight = legendElem.attr('height'); + const legendWidth = Math.floor(legendElem.outerWidth()) - 30; + const legendHeight = legendElem.attr('height'); let rangeStep = 1; if (rangeTo - rangeFrom > legendWidth) { rangeStep = Math.floor((rangeTo - rangeFrom) / legendWidth); } - let widthFactor = legendWidth / (rangeTo - rangeFrom); - let valuesRange = d3.range(rangeFrom, rangeTo, rangeStep); + const widthFactor = legendWidth / (rangeTo - rangeFrom); + const valuesRange = d3.range(rangeFrom, rangeTo, rangeStep); - let colorScale = getColorScale(colorScheme, contextSrv.user.lightTheme, maxValue, minValue); + const colorScale = getColorScale(colorScheme, contextSrv.user.lightTheme, maxValue, minValue); legend .selectAll('.heatmap-color-legend-rect') .data(valuesRange) @@ -120,21 +120,21 @@ function drawColorLegend(elem, colorScheme, rangeFrom, rangeTo, maxValue, minVal } function drawOpacityLegend(elem, options, rangeFrom, rangeTo, maxValue, minValue) { - let legendElem = $(elem).find('svg'); - let legend = d3.select(legendElem.get(0)); + const legendElem = $(elem).find('svg'); + const legend = d3.select(legendElem.get(0)); clearLegend(elem); - let legendWidth = Math.floor(legendElem.outerWidth()) - 30; - let legendHeight = legendElem.attr('height'); + const legendWidth = Math.floor(legendElem.outerWidth()) - 30; + const legendHeight = legendElem.attr('height'); let rangeStep = 1; if (rangeTo - rangeFrom > legendWidth) { rangeStep = Math.floor((rangeTo - rangeFrom) / legendWidth); } - let widthFactor = legendWidth / (rangeTo - rangeFrom); - let valuesRange = d3.range(rangeFrom, rangeTo, rangeStep); + const widthFactor = legendWidth / (rangeTo - rangeFrom); + const valuesRange = d3.range(rangeFrom, rangeTo, rangeStep); - let opacityScale = getOpacityScale(options, maxValue, minValue); + const opacityScale = getOpacityScale(options, maxValue, minValue); legend .selectAll('.heatmap-opacity-legend-rect') .data(valuesRange) @@ -152,27 +152,27 @@ function drawOpacityLegend(elem, options, rangeFrom, rangeTo, maxValue, minValue } function drawLegendValues(elem, colorScale, rangeFrom, rangeTo, maxValue, minValue, legendWidth) { - let legendElem = $(elem).find('svg'); - let legend = d3.select(legendElem.get(0)); + const legendElem = $(elem).find('svg'); + const legend = d3.select(legendElem.get(0)); if (legendWidth <= 0 || legendElem.get(0).childNodes.length === 0) { return; } - let legendValueScale = d3 + const legendValueScale = d3 .scaleLinear() .domain([0, rangeTo]) .range([0, legendWidth]); - let ticks = buildLegendTicks(0, rangeTo, maxValue, minValue); - let xAxis = d3 + const ticks = buildLegendTicks(0, rangeTo, maxValue, minValue); + const xAxis = d3 .axisBottom(legendValueScale) .tickValues(ticks) .tickSize(LEGEND_TICK_SIZE); - let colorRect = legendElem.find(':first-child'); - let posY = getSvgElemHeight(legendElem) + LEGEND_VALUE_MARGIN; - let posX = getSvgElemX(colorRect); + const colorRect = legendElem.find(':first-child'); + const posY = getSvgElemHeight(legendElem) + LEGEND_VALUE_MARGIN; + const posX = getSvgElemX(colorRect); d3 .select(legendElem.get(0)) @@ -188,18 +188,18 @@ function drawLegendValues(elem, colorScale, rangeFrom, rangeTo, maxValue, minVal } function drawSimpleColorLegend(elem, colorScale) { - let legendElem = $(elem).find('svg'); + const legendElem = $(elem).find('svg'); clearLegend(elem); - let legendWidth = Math.floor(legendElem.outerWidth()); - let legendHeight = legendElem.attr('height'); + const legendWidth = Math.floor(legendElem.outerWidth()); + const legendHeight = legendElem.attr('height'); if (legendWidth) { - let valuesNumber = Math.floor(legendWidth / 2); - let rangeStep = Math.floor(legendWidth / valuesNumber); - let valuesRange = d3.range(0, legendWidth, rangeStep); + const valuesNumber = Math.floor(legendWidth / 2); + const rangeStep = Math.floor(legendWidth / valuesNumber); + const valuesRange = d3.range(0, legendWidth, rangeStep); - let legend = d3.select(legendElem.get(0)); + const legend = d3.select(legendElem.get(0)); var legendRects = legend.selectAll('.heatmap-color-legend-rect').data(valuesRange); legendRects @@ -215,12 +215,12 @@ function drawSimpleColorLegend(elem, colorScale) { } function drawSimpleOpacityLegend(elem, options) { - let legendElem = $(elem).find('svg'); + const legendElem = $(elem).find('svg'); clearLegend(elem); - let legend = d3.select(legendElem.get(0)); - let legendWidth = Math.floor(legendElem.outerWidth()); - let legendHeight = legendElem.attr('height'); + const legend = d3.select(legendElem.get(0)); + const legendWidth = Math.floor(legendElem.outerWidth()); + const legendHeight = legendElem.attr('height'); if (legendWidth) { let legendOpacityScale; @@ -237,8 +237,8 @@ function drawSimpleOpacityLegend(elem, options) { .range([0, 1]); } - let rangeStep = 10; - let valuesRange = d3.range(0, legendWidth, rangeStep); + const rangeStep = 10; + const valuesRange = d3.range(0, legendWidth, rangeStep); var legendRects = legend.selectAll('.heatmap-opacity-legend-rect').data(valuesRange); legendRects @@ -255,12 +255,12 @@ function drawSimpleOpacityLegend(elem, options) { } function clearLegend(elem) { - let legendElem = $(elem).find('svg'); + const legendElem = $(elem).find('svg'); legendElem.empty(); } function getSvgElemX(elem) { - let svgElem = elem.get(0); + const svgElem = elem.get(0); if (svgElem && svgElem.x && svgElem.x.baseVal) { return svgElem.x.baseVal.value; } else { @@ -269,7 +269,7 @@ function getSvgElemX(elem) { } function getSvgElemHeight(elem) { - let svgElem = elem.get(0); + const svgElem = elem.get(0); if (svgElem && svgElem.height && svgElem.height.baseVal) { return svgElem.height.baseVal.value; } else { @@ -278,13 +278,13 @@ function getSvgElemHeight(elem) { } function buildLegendTicks(rangeFrom, rangeTo, maxValue, minValue) { - let range = rangeTo - rangeFrom; - let tickStepSize = tickStep(rangeFrom, rangeTo, 3); - let ticksNum = Math.round(range / tickStepSize); + const range = rangeTo - rangeFrom; + const tickStepSize = tickStep(rangeFrom, rangeTo, 3); + const ticksNum = Math.round(range / tickStepSize); let ticks = []; for (let i = 0; i < ticksNum; i++) { - let current = tickStepSize * i; + const current = tickStepSize * i; // Add user-defined min and max if it had been set if (isValueCloseTo(minValue, current, tickStepSize)) { ticks.push(minValue); @@ -309,6 +309,6 @@ function buildLegendTicks(rangeFrom, rangeTo, maxValue, minValue) { } function isValueCloseTo(val, valueTo, step) { - let diff = Math.abs(val - valueTo); + const diff = Math.abs(val - valueTo); return diff < step * 0.3; } diff --git a/public/app/plugins/panel/heatmap/color_scale.ts b/public/app/plugins/panel/heatmap/color_scale.ts index 3550c981db2..2234deb8405 100644 --- a/public/app/plugins/panel/heatmap/color_scale.ts +++ b/public/app/plugins/panel/heatmap/color_scale.ts @@ -2,11 +2,11 @@ import * as d3 from 'd3'; import * as d3ScaleChromatic from 'd3-scale-chromatic'; export function getColorScale(colorScheme: any, lightTheme: boolean, maxValue: number, minValue = 0): (d: any) => any { - let colorInterpolator = d3ScaleChromatic[colorScheme.value]; - let colorScaleInverted = colorScheme.invert === 'always' || colorScheme.invert === (lightTheme ? 'light' : 'dark'); + const colorInterpolator = d3ScaleChromatic[colorScheme.value]; + const colorScaleInverted = colorScheme.invert === 'always' || colorScheme.invert === (lightTheme ? 'light' : 'dark'); - let start = colorScaleInverted ? maxValue : minValue; - let end = colorScaleInverted ? minValue : maxValue; + const start = colorScaleInverted ? maxValue : minValue; + const end = colorScaleInverted ? minValue : maxValue; return d3.scaleSequential(colorInterpolator).domain([start, end]); } diff --git a/public/app/plugins/panel/heatmap/heatmap_ctrl.ts b/public/app/plugins/panel/heatmap/heatmap_ctrl.ts index 1d35ff2ea84..66b72f8d37a 100644 --- a/public/app/plugins/panel/heatmap/heatmap_ctrl.ts +++ b/public/app/plugins/panel/heatmap/heatmap_ctrl.ts @@ -13,10 +13,10 @@ import { sortSeriesByLabel, } from './heatmap_data_converter'; -let X_BUCKET_NUMBER_DEFAULT = 30; -let Y_BUCKET_NUMBER_DEFAULT = 10; +const X_BUCKET_NUMBER_DEFAULT = 30; +const Y_BUCKET_NUMBER_DEFAULT = 10; -let panelDefaults = { +const panelDefaults = { heatmap: {}, cards: { cardPadding: null, @@ -57,12 +57,12 @@ let panelDefaults = { highlightCards: true, }; -let colorModes = ['opacity', 'spectrum']; -let opacityScales = ['linear', 'sqrt']; +const colorModes = ['opacity', 'spectrum']; +const opacityScales = ['linear', 'sqrt']; // Schemes from d3-scale-chromatic // https://github.com/d3/d3-scale-chromatic -let colorSchemes = [ +const colorSchemes = [ // Diverging { name: 'Spectral', value: 'interpolateSpectral', invert: 'always' }, { name: 'RdYlGn', value: 'interpolateRdYlGn', invert: 'always' }, @@ -161,11 +161,11 @@ export class HeatmapCtrl extends MetricsPanelCtrl { let xBucketSize, yBucketSize, bucketsData, heatmapStats; const logBase = this.panel.yAxis.logBase; - let xBucketNumber = this.panel.xBucketNumber || X_BUCKET_NUMBER_DEFAULT; - let xBucketSizeByNumber = Math.floor((this.range.to - this.range.from) / xBucketNumber); + const xBucketNumber = this.panel.xBucketNumber || X_BUCKET_NUMBER_DEFAULT; + const xBucketSizeByNumber = Math.floor((this.range.to - this.range.from) / xBucketNumber); // Parse X bucket size (number or interval) - let isIntervalString = kbn.interval_regex.test(this.panel.xBucketSize); + const isIntervalString = kbn.interval_regex.test(this.panel.xBucketSize); if (isIntervalString) { xBucketSize = kbn.interval_to_ms(this.panel.xBucketSize); } else if ( @@ -180,7 +180,7 @@ export class HeatmapCtrl extends MetricsPanelCtrl { // Calculate Y bucket size heatmapStats = this.parseSeries(this.series); - let yBucketNumber = this.panel.yBucketNumber || Y_BUCKET_NUMBER_DEFAULT; + const yBucketNumber = this.panel.yBucketNumber || Y_BUCKET_NUMBER_DEFAULT; if (logBase !== 1) { yBucketSize = this.panel.yAxis.splitFactor; } else { @@ -204,7 +204,7 @@ export class HeatmapCtrl extends MetricsPanelCtrl { yBucketSize = 1; } - let { cards, cardStats } = convertToCards(bucketsData); + const { cards, cardStats } = convertToCards(bucketsData); this.data = { buckets: bucketsData, @@ -241,12 +241,12 @@ export class HeatmapCtrl extends MetricsPanelCtrl { } // Calculate bucket size based on heatmap data - let xBucketBoundSet = _.map(_.keys(bucketsData), key => Number(key)); + const xBucketBoundSet = _.map(_.keys(bucketsData), key => Number(key)); xBucketSize = calculateBucketSize(xBucketBoundSet); // Always let yBucketSize=1 in 'tsbuckets' mode yBucketSize = 1; - let { cards, cardStats } = convertToCards(bucketsData); + const { cards, cardStats } = convertToCards(bucketsData); this.data = { buckets: bucketsData, @@ -284,7 +284,7 @@ export class HeatmapCtrl extends MetricsPanelCtrl { tip: 'No datapoints returned from data query', }; } else { - for (let series of this.series) { + for (const series of this.series) { if (series.isOutsideRange) { this.dataWarning = { title: 'Data points outside time range', @@ -313,17 +313,17 @@ export class HeatmapCtrl extends MetricsPanelCtrl { throw new Error('Heatmap error: data should be a time series'); } - let series = new TimeSeries({ + const series = new TimeSeries({ datapoints: seriesData.datapoints, alias: seriesData.target, }); series.flotpairs = series.getFlotPairs(this.panel.nullPointMode); - let datapoints = seriesData.datapoints || []; + const datapoints = seriesData.datapoints || []; if (datapoints && datapoints.length > 0) { - let last = datapoints[datapoints.length - 1][1]; - let from = this.range.from; + const last = datapoints[datapoints.length - 1][1]; + const from = this.range.from; if (last - from < -10000) { series.isOutsideRange = true; } @@ -333,9 +333,9 @@ export class HeatmapCtrl extends MetricsPanelCtrl { } parseSeries(series) { - let min = _.min(_.map(series, s => s.stats.min)); - let minLog = _.min(_.map(series, s => s.stats.logmin)); - let max = _.max(_.map(series, s => s.stats.max)); + const min = _.min(_.map(series, s => s.stats.min)); + const minLog = _.min(_.map(series, s => s.stats.logmin)); + const max = _.max(_.map(series, s => s.stats.max)); return { max: max, @@ -345,10 +345,10 @@ export class HeatmapCtrl extends MetricsPanelCtrl { } parseHistogramSeries(series) { - let bounds = _.map(series, s => Number(s.alias)); - let min = _.min(bounds); - let minLog = _.min(bounds); - let max = _.max(bounds); + const bounds = _.map(series, s => Number(s.alias)); + const min = _.min(bounds); + const minLog = _.min(bounds); + const max = _.max(bounds); return { max: max, diff --git a/public/app/plugins/panel/heatmap/heatmap_data_converter.ts b/public/app/plugins/panel/heatmap/heatmap_data_converter.ts index 048b19de911..0b3f83bbe46 100644 --- a/public/app/plugins/panel/heatmap/heatmap_data_converter.ts +++ b/public/app/plugins/panel/heatmap/heatmap_data_converter.ts @@ -1,7 +1,7 @@ import _ from 'lodash'; -let VALUE_INDEX = 0; -let TIME_INDEX = 1; +const VALUE_INDEX = 0; +const TIME_INDEX = 1; interface XBucket { x: number; @@ -18,18 +18,18 @@ interface YBucket { * @param seriesList List of time series */ function histogramToHeatmap(seriesList) { - let heatmap = {}; + const heatmap = {}; for (let i = 0; i < seriesList.length; i++) { - let series = seriesList[i]; - let bound = i; + const series = seriesList[i]; + const bound = i; if (isNaN(bound)) { return heatmap; } - for (let point of series.datapoints) { - let count = point[VALUE_INDEX]; - let time = point[TIME_INDEX]; + for (const point of series.datapoints) { + const count = point[VALUE_INDEX]; + const time = point[TIME_INDEX]; if (!_.isNumber(count)) { continue; @@ -101,10 +101,10 @@ function parseHistogramLabel(label: string): number { function convertToCards(buckets) { let min = 0, max = 0; - let cards = []; + const cards = []; _.forEach(buckets, xBucket => { _.forEach(xBucket.buckets, yBucket => { - let card = { + const card = { x: xBucket.x, y: yBucket.y, yBounds: yBucket.bounds, @@ -123,7 +123,7 @@ function convertToCards(buckets) { }); }); - let cardStats = { min, max }; + const cardStats = { min, max }; return { cards, cardStats }; } @@ -146,19 +146,19 @@ function convertToCards(buckets) { */ function mergeZeroBuckets(buckets, minValue) { _.forEach(buckets, xBucket => { - let yBuckets = xBucket.buckets; + const yBuckets = xBucket.buckets; - let emptyBucket = { + const emptyBucket = { bounds: { bottom: 0, top: 0 }, values: [], points: [], count: 0, }; - let nullBucket = yBuckets[0] || emptyBucket; - let minBucket = yBuckets[minValue] || emptyBucket; + const nullBucket = yBuckets[0] || emptyBucket; + const minBucket = yBuckets[minValue] || emptyBucket; - let newBucket = { + const newBucket = { y: 0, bounds: { bottom: minValue, top: minBucket.bounds.top || minValue }, values: [], @@ -211,11 +211,11 @@ function mergeZeroBuckets(buckets, minValue) { * } */ function convertToHeatMap(seriesList, yBucketSize, xBucketSize, logBase = 1) { - let heatmap = {}; + const heatmap = {}; - for (let series of seriesList) { - let datapoints = series.datapoints; - let seriesName = series.label; + for (const series of seriesList) { + const datapoints = series.datapoints; + const seriesName = series.label; // Slice series into X axis buckets // | | ** | | * | **| @@ -224,7 +224,7 @@ function convertToHeatMap(seriesList, yBucketSize, xBucketSize, logBase = 1) { // |____|____|____|____|____|_ // _.forEach(datapoints, point => { - let bucketBound = getBucketBound(point[TIME_INDEX], xBucketSize); + const bucketBound = getBucketBound(point[TIME_INDEX], xBucketSize); pushToXBuckets(heatmap, point, bucketBound, seriesName); }); } @@ -247,13 +247,13 @@ function convertToHeatMap(seriesList, yBucketSize, xBucketSize, logBase = 1) { } function pushToXBuckets(buckets, point, bucketNum, seriesName) { - let value = point[VALUE_INDEX]; + const value = point[VALUE_INDEX]; if (value === null || value === undefined || isNaN(value)) { return; } // Add series name to point for future identification - let point_ext = _.concat(point, seriesName); + const point_ext = _.concat(point, seriesName); if (buckets[bucketNum] && buckets[bucketNum].values) { buckets[bucketNum].values.push(value); @@ -308,18 +308,18 @@ function getBucketBounds(value, bucketSize) { } function getBucketBound(value, bucketSize) { - let bounds = getBucketBounds(value, bucketSize); + const bounds = getBucketBounds(value, bucketSize); return bounds.bottom; } function convertToValueBuckets(xBucket, bucketSize) { - let values = xBucket.values; - let points = xBucket.points; - let buckets = {}; + const values = xBucket.values; + const points = xBucket.points; + const buckets = {}; _.forEach(values, (val, index) => { - let bounds = getBucketBounds(val, bucketSize); - let bucketNum = bounds.bottom; + const bounds = getBucketBounds(val, bucketSize); + const bucketNum = bounds.bottom; pushToYBuckets(buckets, bucketNum, val, points[index], bounds); }); @@ -335,13 +335,13 @@ function getLogScaleBucketBounds(value, yBucketSplitFactor, logBase) { return { bottom: 0, top: 0 }; } - let value_log = logp(value, logBase); + const value_log = logp(value, logBase); let pow, powTop; if (yBucketSplitFactor === 1 || !yBucketSplitFactor) { pow = Math.floor(value_log); powTop = pow + 1; } else { - let additional_bucket_size = 1 / yBucketSplitFactor; + const additional_bucket_size = 1 / yBucketSplitFactor; let additional_log = value_log - Math.floor(value_log); additional_log = Math.floor(additional_log / additional_bucket_size) * additional_bucket_size; pow = Math.floor(value_log) + additional_log; @@ -354,18 +354,18 @@ function getLogScaleBucketBounds(value, yBucketSplitFactor, logBase) { } function getLogScaleBucketBound(value, yBucketSplitFactor, logBase) { - let bounds = getLogScaleBucketBounds(value, yBucketSplitFactor, logBase); + const bounds = getLogScaleBucketBounds(value, yBucketSplitFactor, logBase); return bounds.bottom; } function convertToLogScaleValueBuckets(xBucket, yBucketSplitFactor, logBase) { - let values = xBucket.values; - let points = xBucket.points; + const values = xBucket.values; + const points = xBucket.points; - let buckets = {}; + const buckets = {}; _.forEach(values, (val, index) => { - let bounds = getLogScaleBucketBounds(val, yBucketSplitFactor, logBase); - let bucketNum = bounds.bottom; + const bounds = getLogScaleBucketBounds(val, yBucketSplitFactor, logBase); + const bucketNum = bounds.bottom; pushToYBuckets(buckets, bucketNum, val, points[index], bounds); }); @@ -396,7 +396,7 @@ function calculateBucketSize(bounds: number[], logBase = 1): number { } else { bounds = _.sortBy(bounds); for (let i = 1; i < bounds.length; i++) { - let distance = getDistance(bounds[i], bounds[i - 1], logBase); + const distance = getDistance(bounds[i], bounds[i - 1], logBase); bucketSize = distance < bucketSize ? distance : bucketSize; } } @@ -416,7 +416,7 @@ function getDistance(a: number, b: number, logBase = 1): number { return Math.abs(b - a); } else { // logarithmic distance - let ratio = Math.max(a, b) / Math.min(a, b); + const ratio = Math.max(a, b) / Math.min(a, b); return logp(ratio, logBase); } } diff --git a/public/app/plugins/panel/heatmap/heatmap_tooltip.ts b/public/app/plugins/panel/heatmap/heatmap_tooltip.ts index 6cf9262f520..5e48849ca59 100644 --- a/public/app/plugins/panel/heatmap/heatmap_tooltip.ts +++ b/public/app/plugins/panel/heatmap/heatmap_tooltip.ts @@ -4,10 +4,10 @@ import _ from 'lodash'; import kbn from 'app/core/utils/kbn'; import { getValueBucketBound } from './heatmap_data_converter'; -let TOOLTIP_PADDING_X = 30; -let TOOLTIP_PADDING_Y = 5; -let HISTOGRAM_WIDTH = 160; -let HISTOGRAM_HEIGHT = 40; +const TOOLTIP_PADDING_X = 30; +const TOOLTIP_PADDING_Y = 5; +const HISTOGRAM_WIDTH = 160; +const HISTOGRAM_HEIGHT = 40; export class HeatmapTooltip { tooltip: any; @@ -67,7 +67,7 @@ export class HeatmapTooltip { return; } - let { xBucketIndex, yBucketIndex } = this.getBucketIndexes(pos, data); + const { xBucketIndex, yBucketIndex } = this.getBucketIndexes(pos, data); if (!data.buckets[xBucketIndex]) { this.destroy(); @@ -79,14 +79,14 @@ export class HeatmapTooltip { } let boundBottom, boundTop, valuesNumber; - let xData = data.buckets[xBucketIndex]; + const xData = data.buckets[xBucketIndex]; // Search in special 'zero' bucket also - let yData = _.find(xData.buckets, (bucket, bucketIndex) => { + const yData = _.find(xData.buckets, (bucket, bucketIndex) => { return bucket.bounds.bottom === yBucketIndex || bucketIndex === yBucketIndex.toString(); }); - let tooltipTimeFormat = 'YYYY-MM-DD HH:mm:ss'; - let time = this.dashboard.formatDate(xData.x, tooltipTimeFormat); + const tooltipTimeFormat = 'YYYY-MM-DD HH:mm:ss'; + const time = this.dashboard.formatDate(xData.x, tooltipTimeFormat); // Decimals override. Code from panel/graph/graph.ts let countValueFormatter, bucketBoundFormatter; @@ -97,7 +97,7 @@ export class HeatmapTooltip { // auto decimals // legend and tooltip gets one more decimal precision // than graph legend ticks - let decimals = (this.panelCtrl.decimals || -1) + 1; + const decimals = (this.panelCtrl.decimals || -1) + 1; countValueFormatter = this.countValueFormatter(decimals, this.panelCtrl.scaledDecimals + 2); bucketBoundFormatter = this.panelCtrl.tickValueFormatter(decimals, this.panelCtrl.scaledDecimals + 2); } @@ -117,7 +117,7 @@ export class HeatmapTooltip { boundTop = yBucketIndex < data.tsBuckets.length - 1 ? tickFormatter(yBucketIndex + 1) : ''; } else { // Display 0 if bucket is a special 'zero' bucket - let bottom = yData.y ? yData.bounds.bottom : 0; + const bottom = yData.y ? yData.bounds.bottom : 0; boundBottom = bucketBoundFormatter(bottom); boundTop = bucketBoundFormatter(yData.bounds.top); } @@ -158,7 +158,7 @@ export class HeatmapTooltip { getXBucketIndex(x, data) { // First try to find X bucket by checking x pos is in the // [bucket.x, bucket.x + xBucketSize] interval - let xBucket = _.find(data.buckets, bucket => { + const xBucket = _.find(data.buckets, bucket => { return x > bucket.x && x - bucket.x <= data.xBucketSize; }); return xBucket ? xBucket.x : getValueBucketBound(x, data.xBucketSize, 1); @@ -168,7 +168,7 @@ export class HeatmapTooltip { if (data.tsBuckets) { return Math.floor(y); } - let yBucketIndex = getValueBucketBound(y, data.yBucketSize, this.panel.yAxis.logBase); + const yBucketIndex = getValueBucketBound(y, data.yBucketSize, this.panel.yAxis.logBase); return yBucketIndex; } @@ -180,8 +180,8 @@ export class HeatmapTooltip { } addHistogram(data) { - let xBucket = this.scope.ctrl.data.buckets[data.x]; - let yBucketSize = this.scope.ctrl.data.yBucketSize; + const xBucket = this.scope.ctrl.data.buckets[data.x]; + const yBucketSize = this.scope.ctrl.data.yBucketSize; let min, max, ticks; if (this.scope.ctrl.data.tsBuckets) { min = 0; @@ -193,33 +193,33 @@ export class HeatmapTooltip { ticks = this.scope.ctrl.data.yAxis.ticks; } let histogramData = _.map(xBucket.buckets, bucket => { - let count = bucket.count !== undefined ? bucket.count : bucket.values.length; + const count = bucket.count !== undefined ? bucket.count : bucket.values.length; return [bucket.bounds.bottom, count]; }); histogramData = _.filter(histogramData, d => { return d[0] >= min && d[0] <= max; }); - let scale = this.scope.yScale.copy(); - let histXScale = scale.domain([min, max]).range([0, HISTOGRAM_WIDTH]); + const scale = this.scope.yScale.copy(); + const histXScale = scale.domain([min, max]).range([0, HISTOGRAM_WIDTH]); let barWidth; if (this.panel.yAxis.logBase === 1) { barWidth = Math.floor(HISTOGRAM_WIDTH / (max - min) * yBucketSize * 0.9); } else { - let barNumberFactor = yBucketSize ? yBucketSize : 1; + const barNumberFactor = yBucketSize ? yBucketSize : 1; barWidth = Math.floor(HISTOGRAM_WIDTH / ticks / barNumberFactor * 0.9); } barWidth = Math.max(barWidth, 1); // Normalize histogram Y axis - let histogramDomain = _.reduce(_.map(histogramData, d => d[1]), (sum, val) => sum + val, 0); - let histYScale = d3 + const histogramDomain = _.reduce(_.map(histogramData, d => d[1]), (sum, val) => sum + val, 0); + const histYScale = d3 .scaleLinear() .domain([0, histogramDomain]) .range([0, HISTOGRAM_HEIGHT]); - let histogram = this.tooltip + const histogram = this.tooltip .select('.heatmap-histogram') .append('svg') .attr('width', HISTOGRAM_WIDTH) @@ -247,9 +247,9 @@ export class HeatmapTooltip { return; } - let elem = $(this.tooltip.node())[0]; - let tooltipWidth = elem.clientWidth; - let tooltipHeight = elem.clientHeight; + const elem = $(this.tooltip.node())[0]; + const tooltipWidth = elem.clientWidth; + const tooltipHeight = elem.clientHeight; let left = pos.pageX + TOOLTIP_PADDING_X; let top = pos.pageY + TOOLTIP_PADDING_Y; @@ -266,7 +266,7 @@ export class HeatmapTooltip { } countValueFormatter(decimals, scaledDecimals = null) { - let format = 'short'; + const format = 'short'; return function(value) { return kbn.valueFormats[format](value, decimals, scaledDecimals); }; diff --git a/public/app/plugins/panel/heatmap/rendering.ts b/public/app/plugins/panel/heatmap/rendering.ts index 8ea216be89d..fcbb39f8417 100644 --- a/public/app/plugins/panel/heatmap/rendering.ts +++ b/public/app/plugins/panel/heatmap/rendering.ts @@ -9,7 +9,7 @@ import { HeatmapTooltip } from './heatmap_tooltip'; import { mergeZeroBuckets } from './heatmap_data_converter'; import { getColorScale, getOpacityScale } from './color_scale'; -let MIN_CARD_SIZE = 1, +const MIN_CARD_SIZE = 1, CARD_PADDING = 1, CARD_ROUND = 0, DATA_RANGE_WIDING_FACTOR = 1.2, @@ -117,8 +117,8 @@ export class HeatmapRenderer { } getYAxisWidth(elem) { - let axis_text = elem.selectAll('.axis-y text').nodes(); - let max_text_width = _.max( + const axis_text = elem.selectAll('.axis-y text').nodes(); + const max_text_width = _.max( _.map(axis_text, text => { // Use SVG getBBox method return text.getBBox().width; @@ -129,10 +129,10 @@ export class HeatmapRenderer { } getXAxisHeight(elem) { - let axis_line = elem.select('.axis-x line'); + const axis_line = elem.select('.axis-x line'); if (!axis_line.empty()) { - let axis_line_position = parseFloat(elem.select('.axis-x line').attr('y2')); - let canvas_width = parseFloat(elem.attr('height')); + const axis_line_position = parseFloat(elem.select('.axis-x line').attr('y2')); + const canvas_width = parseFloat(elem.attr('height')); return canvas_width - axis_line_position; } else { // Default height @@ -146,25 +146,25 @@ export class HeatmapRenderer { .domain([this.timeRange.from, this.timeRange.to]) .range([0, this.chartWidth]); - let ticks = this.chartWidth / DEFAULT_X_TICK_SIZE_PX; - let grafanaTimeFormatter = ticksUtils.grafanaTimeFormat(ticks, this.timeRange.from, this.timeRange.to); + const ticks = this.chartWidth / DEFAULT_X_TICK_SIZE_PX; + const grafanaTimeFormatter = ticksUtils.grafanaTimeFormat(ticks, this.timeRange.from, this.timeRange.to); let timeFormat; - let dashboardTimeZone = this.ctrl.dashboard.getTimezone(); + const dashboardTimeZone = this.ctrl.dashboard.getTimezone(); if (dashboardTimeZone === 'utc') { timeFormat = d3.utcFormat(grafanaTimeFormatter); } else { timeFormat = d3.timeFormat(grafanaTimeFormatter); } - let xAxis = d3 + const xAxis = d3 .axisBottom(this.xScale) .ticks(ticks) .tickFormat(timeFormat) .tickPadding(X_AXIS_TICK_PADDING) .tickSize(this.chartHeight); - let posY = this.margin.top; - let posX = this.yAxisWidth; + const posY = this.margin.top; + const posX = this.yAxisWidth; this.heatmap .append('g') .attr('class', 'axis axis-x') @@ -191,11 +191,11 @@ export class HeatmapRenderer { tick_interval = ticksUtils.tickStep(y_min, y_max, ticks); ticks = Math.ceil((y_max - y_min) / tick_interval); - let decimalsAuto = ticksUtils.getPrecision(tick_interval); + const decimalsAuto = ticksUtils.getPrecision(tick_interval); let decimals = this.panel.yAxis.decimals === null ? decimalsAuto : this.panel.yAxis.decimals; // Calculate scaledDecimals for log scales using tick size (as in jquery.flot.js) - let flot_tick_size = ticksUtils.getFlotTickSize(y_min, y_max, ticks, decimalsAuto); - let scaledDecimals = ticksUtils.getScaledDecimals(decimals, flot_tick_size); + const flot_tick_size = ticksUtils.getFlotTickSize(y_min, y_max, ticks, decimalsAuto); + const scaledDecimals = ticksUtils.getScaledDecimals(decimals, flot_tick_size); this.ctrl.decimals = decimals; this.ctrl.scaledDecimals = scaledDecimals; @@ -218,7 +218,7 @@ export class HeatmapRenderer { .domain([y_min, y_max]) .range([this.chartHeight, 0]); - let yAxis = d3 + const yAxis = d3 .axisLeft(this.yScale) .ticks(ticks) .tickFormat(this.tickValueFormatter(decimals, scaledDecimals)) @@ -232,8 +232,8 @@ export class HeatmapRenderer { .call(yAxis); // Calculate Y axis width first, then move axis into visible area - let posY = this.margin.top; - let posX = this.getYAxisWidth(this.heatmap) + Y_AXIS_TICK_PADDING; + const posY = this.margin.top; + const posX = this.getYAxisWidth(this.heatmap) + Y_AXIS_TICK_PADDING; this.heatmap.select('.axis-y').attr('transform', 'translate(' + posX + ',' + posY + ')'); // Remove vertical line in the right of axis labels (called domain in d3) @@ -245,7 +245,7 @@ export class HeatmapRenderer { // Wide Y values range and anjust to bucket size wideYAxisRange(min, max, tickInterval) { - let y_widing = (max * (this.dataRangeWidingFactor - 1) - min * (this.dataRangeWidingFactor - 1)) / 2; + const y_widing = (max * (this.dataRangeWidingFactor - 1) - min * (this.dataRangeWidingFactor - 1)) / 2; let y_min, y_max; if (tickInterval === 0) { @@ -266,7 +266,7 @@ export class HeatmapRenderer { } addLogYAxis() { - let log_base = this.panel.yAxis.logBase; + const log_base = this.panel.yAxis.logBase; let { y_min, y_max } = this.adjustLogRange(this.data.heatmapStats.minLog, this.data.heatmapStats.max, log_base); y_min = @@ -285,15 +285,15 @@ export class HeatmapRenderer { .domain([y_min, y_max]) .range([this.chartHeight, 0]); - let domain = this.yScale.domain(); - let tick_values = this.logScaleTickValues(domain, log_base); + const domain = this.yScale.domain(); + const tick_values = this.logScaleTickValues(domain, log_base); - let decimalsAuto = ticksUtils.getPrecision(y_min); - let decimals = this.panel.yAxis.decimals || decimalsAuto; + const decimalsAuto = ticksUtils.getPrecision(y_min); + const decimals = this.panel.yAxis.decimals || decimalsAuto; // Calculate scaledDecimals for log scales using tick size (as in jquery.flot.js) - let flot_tick_size = ticksUtils.getFlotTickSize(y_min, y_max, tick_values.length, decimalsAuto); - let scaledDecimals = ticksUtils.getScaledDecimals(decimals, flot_tick_size); + const flot_tick_size = ticksUtils.getFlotTickSize(y_min, y_max, tick_values.length, decimalsAuto); + const scaledDecimals = ticksUtils.getScaledDecimals(decimals, flot_tick_size); this.ctrl.decimals = decimals; this.ctrl.scaledDecimals = scaledDecimals; @@ -303,7 +303,7 @@ export class HeatmapRenderer { ticks: tick_values.length, }; - let yAxis = d3 + const yAxis = d3 .axisLeft(this.yScale) .tickValues(tick_values) .tickFormat(this.tickValueFormatter(decimals, scaledDecimals)) @@ -317,8 +317,8 @@ export class HeatmapRenderer { .call(yAxis); // Calculate Y axis width first, then move axis into visible area - let posY = this.margin.top; - let posX = this.getYAxisWidth(this.heatmap) + Y_AXIS_TICK_PADDING; + const posY = this.margin.top; + const posX = this.getYAxisWidth(this.heatmap) + Y_AXIS_TICK_PADDING; this.heatmap.select('.axis-y').attr('transform', 'translate(' + posX + ',' + posY + ')'); // Set first tick as pseudo 0 @@ -349,7 +349,7 @@ export class HeatmapRenderer { const decimals = this.panel.yAxis.decimals === null ? decimalsAuto : this.panel.yAxis.decimals; this.ctrl.decimals = decimals; - let tickValueFormatter = this.tickValueFormatter.bind(this); + const tickValueFormatter = this.tickValueFormatter.bind(this); function tickFormatter(valIndex) { let valueFormatted = tsBuckets[valIndex]; if (!_.isNaN(_.toNumber(valueFormatted)) && valueFormatted !== '') { @@ -362,7 +362,7 @@ export class HeatmapRenderer { const tsBucketsFormatted = _.map(tsBuckets, (v, i) => tickFormatter(i)); this.data.tsBucketsFormatted = tsBucketsFormatted; - let yAxis = d3 + const yAxis = d3 .axisLeft(this.yScale) .tickValues(tick_values) .tickFormat(tickFormatter) @@ -413,21 +413,21 @@ export class HeatmapRenderer { } logScaleTickValues(domain, base) { - let domainMin = domain[0]; - let domainMax = domain[1]; - let tickValues = []; + const domainMin = domain[0]; + const domainMax = domain[1]; + const tickValues = []; if (domainMin < 1) { - let under_one_ticks = Math.floor(ticksUtils.logp(domainMin, base)); + const under_one_ticks = Math.floor(ticksUtils.logp(domainMin, base)); for (let i = under_one_ticks; i < 0; i++) { - let tick_value = Math.pow(base, i); + const tick_value = Math.pow(base, i); tickValues.push(tick_value); } } - let ticks = Math.ceil(ticksUtils.logp(domainMax, base)); + const ticks = Math.ceil(ticksUtils.logp(domainMax, base)); for (let i = 0; i <= ticks; i++) { - let tick_value = Math.pow(base, i); + const tick_value = Math.pow(base, i); tickValues.push(tick_value); } @@ -435,7 +435,7 @@ export class HeatmapRenderer { } tickValueFormatter(decimals, scaledDecimals = null) { - let format = this.panel.yAxis.format; + const format = this.panel.yAxis.format; return function(value) { try { return format !== 'none' ? kbn.valueFormats[format](value, decimals, scaledDecimals) : value; @@ -490,7 +490,7 @@ export class HeatmapRenderer { } addHeatmapCanvas() { - let heatmap_elem = this.$heatmap[0]; + const heatmap_elem = this.$heatmap[0]; this.width = Math.floor(this.$heatmap.width()) - this.padding.right; this.height = Math.floor(this.$heatmap.height()) - this.padding.bottom; @@ -514,18 +514,18 @@ export class HeatmapRenderer { this.addAxes(); if (this.panel.yAxis.logBase !== 1 && this.panel.dataFormat !== 'tsbuckets') { - let log_base = this.panel.yAxis.logBase; - let domain = this.yScale.domain(); - let tick_values = this.logScaleTickValues(domain, log_base); + const log_base = this.panel.yAxis.logBase; + const domain = this.yScale.domain(); + const tick_values = this.logScaleTickValues(domain, log_base); this.data.buckets = mergeZeroBuckets(this.data.buckets, _.min(tick_values)); } - let cardsData = this.data.cards; - let maxValueAuto = this.data.cardStats.max; - let maxValue = this.panel.color.max || maxValueAuto; - let minValue = this.panel.color.min || 0; + const cardsData = this.data.cards; + const maxValueAuto = this.data.cardStats.max; + const maxValue = this.panel.color.max || maxValueAuto; + const minValue = this.panel.color.min || 0; - let colorScheme = _.find(this.ctrl.colorSchemes, { + const colorScheme = _.find(this.ctrl.colorSchemes, { value: this.panel.color.colorScheme, }); this.colorScale = getColorScale(colorScheme, contextSrv.user.lightTheme, maxValue, minValue); @@ -549,7 +549,7 @@ export class HeatmapRenderer { .style('stroke-width', 0) .style('opacity', this.getCardOpacity.bind(this)); - let $cards = this.$heatmap.find('.heatmap-card'); + const $cards = this.$heatmap.find('.heatmap-card'); $cards .on('mouseenter', event => { this.tooltip.mouseOverBucket = true; @@ -562,10 +562,10 @@ export class HeatmapRenderer { } highlightCard(event) { - let color = d3.select(event.target).style('fill'); - let highlightColor = d3.color(color).darker(2); - let strokeColor = d3.color(color).brighter(4); - let current_card = d3.select(event.target); + const color = d3.select(event.target).style('fill'); + const highlightColor = d3.color(color).darker(2); + const strokeColor = d3.color(color).brighter(4); + const current_card = d3.select(event.target); this.tooltip.originalFillColor = color; current_card .style('fill', highlightColor.toString()) @@ -582,12 +582,12 @@ export class HeatmapRenderer { } setCardSize() { - let xGridSize = Math.floor(this.xScale(this.data.xBucketSize) - this.xScale(0)); + const xGridSize = Math.floor(this.xScale(this.data.xBucketSize) - this.xScale(0)); let yGridSize = Math.floor(this.yScale(this.yScale.invert(0) - this.data.yBucketSize)); if (this.panel.yAxis.logBase !== 1) { - let base = this.panel.yAxis.logBase; - let splitFactor = this.data.yBucketSize || 1; + const base = this.panel.yAxis.logBase; + const splitFactor = this.data.yBucketSize || 1; yGridSize = Math.floor((this.yScale(1) - this.yScale(base)) / splitFactor); } @@ -611,7 +611,7 @@ export class HeatmapRenderer { let w; if (this.xScale(d.x) < 0) { // Cut card left to prevent overlay - let cutted_width = this.xScale(d.x) + this.cardWidth; + const cutted_width = this.xScale(d.x) + this.cardWidth; w = cutted_width > 0 ? cutted_width : 0; } else if (this.xScale(d.x) + this.cardWidth > this.chartWidth) { // Cut card right to prevent overlay @@ -639,7 +639,7 @@ export class HeatmapRenderer { } getCardHeight(d) { - let y = this.yScale(d.y) + this.chartTop - this.cardHeight - this.cardPadding; + const y = this.yScale(d.y) + this.chartTop - this.cardHeight - this.cardPadding; let h = this.cardHeight; if (this.panel.yAxis.logBase !== 1 && d.y === 0) { @@ -703,10 +703,10 @@ export class HeatmapRenderer { this.mouseUpHandler = null; this.selection.active = false; - let selectionRange = Math.abs(this.selection.x2 - this.selection.x1); + const selectionRange = Math.abs(this.selection.x2 - this.selection.x1); if (this.selection.x2 >= 0 && selectionRange > MIN_SELECTION_WIDTH) { - let timeFrom = this.xScale.invert(Math.min(this.selection.x1, this.selection.x2) - this.yAxisWidth); - let timeTo = this.xScale.invert(Math.max(this.selection.x1, this.selection.x2) - this.yAxisWidth); + const timeFrom = this.xScale.invert(Math.min(this.selection.x1, this.selection.x2) - this.yAxisWidth); + const timeTo = this.xScale.invert(Math.max(this.selection.x1, this.selection.x2) - this.yAxisWidth); this.ctrl.timeSrv.setTime({ from: moment.utc(timeFrom), @@ -744,9 +744,9 @@ export class HeatmapRenderer { } getEventPos(event, offset) { - let x = this.xScale.invert(offset.x - this.yAxisWidth).valueOf(); - let y = this.yScale.invert(offset.y - this.chartTop); - let pos = { + const x = this.xScale.invert(offset.x - this.yAxisWidth).valueOf(); + const y = this.yScale.invert(offset.y - this.chartTop); + const pos = { pageX: event.pageX, pageY: event.pageY, x: x, @@ -776,8 +776,8 @@ export class HeatmapRenderer { drawSelection(posX1, posX2) { if (this.heatmap) { this.heatmap.selectAll('.heatmap-selection').remove(); - let selectionX = Math.min(posX1, posX2); - let selectionWidth = Math.abs(posX1 - posX2); + const selectionX = Math.min(posX1, posX2); + const selectionWidth = Math.abs(posX1 - posX2); if (selectionWidth > MIN_SELECTION_WIDTH) { this.heatmap @@ -823,7 +823,7 @@ export class HeatmapRenderer { drawSharedCrosshair(pos) { if (this.heatmap && this.ctrl.dashboard.graphTooltip !== 0) { - let posX = this.xScale(pos.x) + this.yAxisWidth; + const posX = this.xScale(pos.x) + this.yAxisWidth; this.drawCrosshair(posX); } } diff --git a/public/app/plugins/panel/heatmap/specs/heatmap_ctrl.test.ts b/public/app/plugins/panel/heatmap/specs/heatmap_ctrl.test.ts index 800c2518f9a..d9d929a2697 100644 --- a/public/app/plugins/panel/heatmap/specs/heatmap_ctrl.test.ts +++ b/public/app/plugins/panel/heatmap/specs/heatmap_ctrl.test.ts @@ -2,13 +2,13 @@ import moment from 'moment'; import { HeatmapCtrl } from '../heatmap_ctrl'; describe('HeatmapCtrl', function() { - let ctx = {}; + const ctx = {}; - let $injector = { + const $injector = { get: () => {}, }; - let $scope = { + const $scope = { $on: () => {}, }; diff --git a/public/app/plugins/panel/heatmap/specs/heatmap_data_converter.test.ts b/public/app/plugins/panel/heatmap/specs/heatmap_data_converter.test.ts index b6a8713a3e9..1c8a7a32caf 100644 --- a/public/app/plugins/panel/heatmap/specs/heatmap_data_converter.test.ts +++ b/public/app/plugins/panel/heatmap/specs/heatmap_data_converter.test.ts @@ -10,7 +10,7 @@ import { } from '../heatmap_data_converter'; describe('isHeatmapDataEqual', () => { - let ctx: any = {}; + const ctx: any = {}; beforeEach(() => { ctx.heatmapA = { @@ -35,17 +35,17 @@ describe('isHeatmapDataEqual', () => { }); it('should proper compare objects', () => { - let heatmapC = _.cloneDeep(ctx.heatmapA); + const heatmapC = _.cloneDeep(ctx.heatmapA); heatmapC['1422774000000'].buckets['1'].values = [1, 1.5]; - let heatmapD = _.cloneDeep(ctx.heatmapA); + const heatmapD = _.cloneDeep(ctx.heatmapA); heatmapD['1422774000000'].buckets['1'].values = [1.5, 1, 1.6]; - let heatmapE = _.cloneDeep(ctx.heatmapA); + const heatmapE = _.cloneDeep(ctx.heatmapA); heatmapE['1422774000000'].buckets['1'].values = [1, 1.6]; - let empty = {}; - let emptyValues = _.cloneDeep(ctx.heatmapA); + const empty = {}; + const emptyValues = _.cloneDeep(ctx.heatmapA); emptyValues['1422774000000'].buckets['1'].values = []; expect(isHeatmapDataEqual(ctx.heatmapA, ctx.heatmapB)).toBe(true); @@ -69,7 +69,7 @@ describe('isHeatmapDataEqual', () => { }); describe('calculateBucketSize', () => { - let ctx: any = {}; + const ctx: any = {}; describe('when logBase is 1 (linear scale)', () => { beforeEach(() => { @@ -88,7 +88,7 @@ describe('calculateBucketSize', () => { it('should properly calculate bucket size', () => { _.each(ctx.bounds_set, b => { - let bucketSize = calculateBucketSize(b.bounds, ctx.logBase); + const bucketSize = calculateBucketSize(b.bounds, ctx.logBase); expect(bucketSize).toBe(b.size); }); }); @@ -108,7 +108,7 @@ describe('calculateBucketSize', () => { it('should properly calculate bucket size', () => { _.each(ctx.bounds_set, b => { - let bucketSize = calculateBucketSize(b.bounds, ctx.logBase); + const bucketSize = calculateBucketSize(b.bounds, ctx.logBase); expect(isEqual(bucketSize, b.size)).toBe(true); }); }); @@ -116,7 +116,7 @@ describe('calculateBucketSize', () => { }); describe('HeatmapDataConverter', () => { - let ctx: any = {}; + const ctx: any = {}; beforeEach(() => { ctx.series = []; @@ -150,7 +150,7 @@ describe('HeatmapDataConverter', () => { }); it('should build proper heatmap data', () => { - let expectedHeatmap = { + const expectedHeatmap = { '1422774000000': { x: 1422774000000, buckets: { @@ -183,7 +183,7 @@ describe('HeatmapDataConverter', () => { }, }; - let heatmap = convertToHeatMap(ctx.series, ctx.yBucketSize, ctx.xBucketSize, ctx.logBase); + const heatmap = convertToHeatMap(ctx.series, ctx.yBucketSize, ctx.xBucketSize, ctx.logBase); expect(isHeatmapDataEqual(heatmap, expectedHeatmap)).toBe(true); }); }); @@ -194,7 +194,7 @@ describe('HeatmapDataConverter', () => { }); it('should build proper heatmap data', () => { - let expectedHeatmap = { + const expectedHeatmap = { '1422774000000': { x: 1422774000000, buckets: { @@ -210,14 +210,14 @@ describe('HeatmapDataConverter', () => { }, }; - let heatmap = convertToHeatMap(ctx.series, ctx.yBucketSize, ctx.xBucketSize, ctx.logBase); + const heatmap = convertToHeatMap(ctx.series, ctx.yBucketSize, ctx.xBucketSize, ctx.logBase); expect(isHeatmapDataEqual(heatmap, expectedHeatmap)).toBe(true); }); }); }); describe('Histogram converter', () => { - let ctx: any = {}; + const ctx: any = {}; beforeEach(() => { ctx.series = []; @@ -248,7 +248,7 @@ describe('Histogram converter', () => { beforeEach(() => {}); it('should build proper heatmap data', () => { - let expectedHeatmap = { + const expectedHeatmap = { '1422774000000': { x: 1422774000000, buckets: { @@ -343,18 +343,18 @@ describe('convertToCards', () => { }); it('should build proper cards data', () => { - let expectedCards = [ + const expectedCards = [ { x: 1422774000000, y: 1, count: 1, values: [1], yBounds: {} }, { x: 1422774000000, y: 2, count: 1, values: [2], yBounds: {} }, { x: 1422774060000, y: 2, count: 2, values: [2, 3], yBounds: {} }, ]; - let res = convertToCards(buckets); + const res = convertToCards(buckets); expect(res.cards).toMatchObject(expectedCards); }); it('should build proper cards stats', () => { - let expectedStats = { min: 1, max: 2 }; - let res = convertToCards(buckets); + const expectedStats = { min: 1, max: 2 }; + const res = convertToCards(buckets); expect(res.cardStats).toMatchObject(expectedStats); }); }); diff --git a/public/app/plugins/panel/pluginlist/module.ts b/public/app/plugins/panel/pluginlist/module.ts index acfa69b171c..93bf258f50d 100644 --- a/public/app/plugins/panel/pluginlist/module.ts +++ b/public/app/plugins/panel/pluginlist/module.ts @@ -60,7 +60,7 @@ class PluginListCtrl extends PanelCtrl { this.viewModel[1].list = _.filter(plugins, { type: 'panel' }); this.viewModel[2].list = _.filter(plugins, { type: 'datasource' }); - for (let plugin of this.pluginList) { + for (const plugin of this.pluginList) { if (plugin.hasUpdate) { plugin.state = 'has-update'; } else if (!plugin.enabled) { diff --git a/public/app/plugins/panel/singlestat/module.ts b/public/app/plugins/panel/singlestat/module.ts index ebd2628b086..b858f77556f 100644 --- a/public/app/plugins/panel/singlestat/module.ts +++ b/public/app/plugins/panel/singlestat/module.ts @@ -293,8 +293,8 @@ class SingleStatCtrl extends MetricsPanelCtrl { } if (this.series && this.series.length > 0) { - let lastPoint = _.last(this.series[0].datapoints); - let lastValue = _.isArray(lastPoint) ? lastPoint[0] : null; + const lastPoint = _.last(this.series[0].datapoints); + const lastValue = _.isArray(lastPoint) ? lastPoint[0] : null; if (this.panel.valueName === 'name') { data.value = 0; @@ -305,7 +305,7 @@ class SingleStatCtrl extends MetricsPanelCtrl { data.valueFormatted = _.escape(lastValue); data.valueRounded = 0; } else if (this.panel.valueName === 'last_time') { - let formatFunc = kbn.valueFormats[this.panel.format]; + const formatFunc = kbn.valueFormats[this.panel.format]; data.value = lastPoint[1]; data.valueRounded = data.value; data.valueFormatted = formatFunc(data.value, this.dashboard.isTimezoneUtc()); @@ -313,8 +313,8 @@ class SingleStatCtrl extends MetricsPanelCtrl { data.value = this.series[0].stats[this.panel.valueName]; data.flotpairs = this.series[0].flotpairs; - let decimalInfo = this.getDecimalsForValue(data.value); - let formatFunc = kbn.valueFormats[this.panel.format]; + const decimalInfo = this.getDecimalsForValue(data.value); + const formatFunc = kbn.valueFormats[this.panel.format]; data.valueFormatted = formatFunc(data.value, decimalInfo.decimals, decimalInfo.scaledDecimals); data.valueRounded = kbn.roundValue(data.value, decimalInfo.decimals); } @@ -330,7 +330,7 @@ class SingleStatCtrl extends MetricsPanelCtrl { // check value to text mappings if its enabled if (this.panel.mappingType === 1) { for (let i = 0; i < this.panel.valueMaps.length; i++) { - let map = this.panel.valueMaps[i]; + const map = this.panel.valueMaps[i]; // special null case if (map.value === 'null') { if (data.value === null || data.value === void 0) { @@ -349,7 +349,7 @@ class SingleStatCtrl extends MetricsPanelCtrl { } } else if (this.panel.mappingType === 2) { for (let i = 0; i < this.panel.rangeMaps.length; i++) { - let map = this.panel.rangeMaps[i]; + const map = this.panel.rangeMaps[i]; // special null case if (map.from === 'null' && map.to === 'null') { if (data.value === null || data.value === void 0) { diff --git a/public/app/plugins/panel/singlestat/specs/singlestat.test.ts b/public/app/plugins/panel/singlestat/specs/singlestat.test.ts index 0480d0be5c3..9d204f19f5b 100644 --- a/public/app/plugins/panel/singlestat/specs/singlestat.test.ts +++ b/public/app/plugins/panel/singlestat/specs/singlestat.test.ts @@ -2,15 +2,15 @@ import { SingleStatCtrl } from '../module'; import moment from 'moment'; describe('SingleStatCtrl', function() { - let ctx = {}; - let epoch = 1505826363746; + const ctx = {}; + const epoch = 1505826363746; Date.now = () => epoch; - let $scope = { + const $scope = { $on: () => {}, }; - let $injector = { + const $injector = { get: () => {}, }; diff --git a/public/app/plugins/panel/table/module.ts b/public/app/plugins/panel/table/module.ts index 03d92f7e48f..4169a25dd43 100644 --- a/public/app/plugins/panel/table/module.ts +++ b/public/app/plugins/panel/table/module.ts @@ -243,7 +243,7 @@ class TablePanelCtrl extends MetricsPanelCtrl { }); function addFilterClicked(e) { - let filterData = $(e.currentTarget).data(); + const filterData = $(e.currentTarget).data(); var options = { datasource: panel.datasource, key: data.columns[filterData.column].text, diff --git a/public/app/plugins/panel/table/renderer.ts b/public/app/plugins/panel/table/renderer.ts index d85c20a87cc..d512c1335df 100644 --- a/public/app/plugins/panel/table/renderer.ts +++ b/public/app/plugins/panel/table/renderer.ts @@ -21,11 +21,11 @@ export class TableRenderer { this.colorState = {}; for (let colIndex = 0; colIndex < this.table.columns.length; colIndex++) { - let column = this.table.columns[colIndex]; + const column = this.table.columns[colIndex]; column.title = column.text; for (let i = 0; i < this.panel.styles.length; i++) { - let style = this.panel.styles[i]; + const style = this.panel.styles[i]; var regex = kbn.stringToJsRegex(style.pattern); if (column.text.match(regex)) { @@ -154,7 +154,7 @@ export class TableRenderer { } if (column.style.type === 'number') { - let valueFormatter = kbn.valueFormats[column.unit || column.style.unit]; + const valueFormatter = kbn.valueFormats[column.unit || column.style.unit]; return v => { if (v === null || v === void 0) { @@ -193,9 +193,9 @@ export class TableRenderer { } renderRowVariables(rowIndex) { - let scopedVars = {}; + const scopedVars = {}; let cell_variable; - let row = this.table.rows[rowIndex]; + const row = this.table.rows[rowIndex]; for (let i = 0; i < row.length; i++) { cell_variable = `__cell_${i}`; scopedVars[cell_variable] = { value: row[i] }; @@ -288,15 +288,15 @@ export class TableRenderer { } render(page) { - let pageSize = this.panel.pageSize || 100; - let startPos = page * pageSize; - let endPos = Math.min(startPos + pageSize, this.table.rows.length); + const pageSize = this.panel.pageSize || 100; + const startPos = page * pageSize; + const endPos = Math.min(startPos + pageSize, this.table.rows.length); var html = ''; - let rowClasses = []; + const rowClasses = []; let rowClass = ''; for (var y = startPos; y < endPos; y++) { - let row = this.table.rows[y]; + const row = this.table.rows[y]; let cellHtml = ''; let rowStyle = ''; for (var i = 0; i < this.table.columns.length; i++) { @@ -320,11 +320,11 @@ export class TableRenderer { } render_values() { - let rows = []; + const rows = []; for (var y = 0; y < this.table.rows.length; y++) { - let row = this.table.rows[y]; - let new_row = []; + const row = this.table.rows[y]; + const new_row = []; for (var i = 0; i < this.table.columns.length; i++) { new_row.push(this.formatColumnValue(i, row[i])); } diff --git a/public/app/plugins/panel/table/transformers.ts b/public/app/plugins/panel/table/transformers.ts index 1659ba3e3aa..840bfa83d5b 100644 --- a/public/app/plugins/panel/table/transformers.ts +++ b/public/app/plugins/panel/table/transformers.ts @@ -294,7 +294,7 @@ transformers['json'] = { transform: function(data, panel, model) { var i, y, z; - for (let column of panel.columns) { + for (const column of panel.columns) { var tableCol: any = { text: column.text }; // if filterable data then set columns to filterable diff --git a/public/app/stores/AlertListStore/AlertListStore.ts b/public/app/stores/AlertListStore/AlertListStore.ts index ec27565a1a1..c2b9f5e4962 100644 --- a/public/app/stores/AlertListStore/AlertListStore.ts +++ b/public/app/stores/AlertListStore/AlertListStore.ts @@ -13,7 +13,7 @@ export const AlertListStore = types }) .views(self => ({ get filteredRules() { - let regex = new RegExp(self.search, 'i'); + const regex = new RegExp(self.search, 'i'); return self.rules.filter(alert => { return regex.test(alert.name) || regex.test(alert.stateText) || regex.test(alert.info); }); @@ -26,7 +26,7 @@ export const AlertListStore = types const apiRules = yield backendSrv.get('/api/alerts', filters); self.rules.clear(); - for (let rule of apiRules) { + for (const rule of apiRules) { setStateFields(rule, rule.state); if (rule.state !== 'paused') { diff --git a/public/app/stores/NavStore/NavStore.ts b/public/app/stores/NavStore/NavStore.ts index bef53b828b6..d869b0f740d 100644 --- a/public/app/stores/NavStore/NavStore.ts +++ b/public/app/stores/NavStore/NavStore.ts @@ -12,9 +12,9 @@ export const NavStore = types load(...args) { let children = getEnv(self).navTree; let main, node; - let parents = []; + const parents = []; - for (let id of args) { + for (const id of args) { node = children.find(el => el.id === id); if (!node) { @@ -28,7 +28,7 @@ export const NavStore = types main = parents[parents.length - 2]; if (main.children) { - for (let item of main.children) { + for (const item of main.children) { item.active = false; if (item.url === node.url) { @@ -42,7 +42,7 @@ export const NavStore = types }, initFolderNav(folder: any, activeChildId: string) { - let main = { + const main = { icon: 'fa fa-folder-open', id: 'manage-folder', subTitle: 'Manage folder dashboards & permissions', @@ -79,13 +79,13 @@ export const NavStore = types initDatasourceEditNav(ds: any, plugin: any, currentPage: string) { let title = 'New'; - let subTitle = `Type: ${plugin.name}`; + const subTitle = `Type: ${plugin.name}`; if (ds.id) { title = ds.name; } - let main = { + const main = { img: plugin.info.logos.large, id: 'ds-edit-' + plugin.id, subTitle: subTitle, @@ -118,7 +118,7 @@ export const NavStore = types }, initTeamPage(team: Team, tab: string, isSyncEnabled: boolean) { - let main = { + const main = { img: team.avatarUrl, id: 'team-' + team.id, subTitle: 'Manage members & settings', diff --git a/public/app/stores/PermissionsStore/PermissionsStore.ts b/public/app/stores/PermissionsStore/PermissionsStore.ts index 95d63c8527a..d778a09443d 100644 --- a/public/app/stores/PermissionsStore/PermissionsStore.ts +++ b/public/app/stores/PermissionsStore/PermissionsStore.ts @@ -117,7 +117,7 @@ export const PermissionsStore = types }), addStoreItem: flow(function* addStoreItem() { - let item = { + const item = { type: self.newItem.type, permission: self.newItem.permission, dashboardId: self.dashboardId, @@ -155,7 +155,7 @@ export const PermissionsStore = types try { yield updateItems(self, updatedItems); self.items.push(newItem); - let sortedItems = self.items.sort((a, b) => b.sortRank - a.sortRank || a.name.localeCompare(b.name)); + const sortedItems = self.items.sort((a, b) => b.sortRank - a.sortRank || a.name.localeCompare(b.name)); self.items = sortedItems; resetNewTypeInternal(); } catch {} @@ -197,7 +197,7 @@ export const PermissionsStore = types const updateItems = (self, items) => { const backendSrv = getEnv(self).backendSrv; const updated = []; - for (let item of items) { + for (const item of items) { if (item.inherited) { continue; } diff --git a/public/app/stores/TeamsStore/TeamsStore.ts b/public/app/stores/TeamsStore/TeamsStore.ts index 1aec4a1433c..f8e101163a2 100644 --- a/public/app/stores/TeamsStore/TeamsStore.ts +++ b/public/app/stores/TeamsStore/TeamsStore.ts @@ -32,8 +32,8 @@ export const TeamModel = types }) .views(self => ({ get filteredMembers() { - let members = this.members.values(); - let regex = new RegExp(self.search, 'i'); + const members = this.members.values(); + const regex = new RegExp(self.search, 'i'); return members.filter(member => { return regex.test(member.login) || regex.test(member.email); }); @@ -66,7 +66,7 @@ export const TeamModel = types const rsp = yield backendSrv.get(`/api/teams/${self.id}/members`); self.members.clear(); - for (let member of rsp) { + for (const member of rsp) { self.members.set(member.userId.toString(), TeamMemberModel.create(member)); } }), @@ -88,7 +88,7 @@ export const TeamModel = types const rsp = yield backendSrv.get(`/api/teams/${self.id}/groups`); self.groups.clear(); - for (let group of rsp) { + for (const group of rsp) { self.groups.set(group.groupId, TeamGroupModel.create(group)); } }), @@ -122,8 +122,8 @@ export const TeamsStore = types }) .views(self => ({ get filteredTeams() { - let teams = this.map.values(); - let regex = new RegExp(self.search, 'i'); + const teams = this.map.values(); + const regex = new RegExp(self.search, 'i'); return teams.filter(team => { return regex.test(team.name); }); @@ -135,7 +135,7 @@ export const TeamsStore = types const rsp = yield backendSrv.get('/api/teams/search/', { perpage: 50, page: 1 }); self.map.clear(); - for (let team of rsp.teams) { + for (const team of rsp.teams) { self.map.set(team.id.toString(), TeamModel.create(team)); } }), diff --git a/public/app/stores/ViewStore/ViewStore.ts b/public/app/stores/ViewStore/ViewStore.ts index ba966a194d8..3af6737209c 100644 --- a/public/app/stores/ViewStore/ViewStore.ts +++ b/public/app/stores/ViewStore/ViewStore.ts @@ -25,7 +25,7 @@ export const ViewStore = types // querystring only function updateQuery(query: any) { self.query.clear(); - for (let key of Object.keys(query)) { + for (const key of Object.keys(query)) { if (query[key]) { self.query.set(key, query[key]); } @@ -35,7 +35,7 @@ export const ViewStore = types // needed to get route parameters like slug from the url function updateRouteParams(routeParams: any) { self.routeParams.clear(); - for (let key of Object.keys(routeParams)) { + for (const key of Object.keys(routeParams)) { if (routeParams[key]) { self.routeParams.set(key, routeParams[key]); } diff --git a/public/test/core/utils/version_test.ts b/public/test/core/utils/version_test.ts index 20983cb32ec..91330389e24 100644 --- a/public/test/core/utils/version_test.ts +++ b/public/test/core/utils/version_test.ts @@ -1,11 +1,11 @@ -import {SemVersion, isVersionGtOrEq} from 'app/core/utils/version'; +import { SemVersion, isVersionGtOrEq } from 'app/core/utils/version'; -describe("SemVersion", () => { +describe('SemVersion', () => { let version = '1.0.0-alpha.1'; describe('parsing', () => { it('should parse version properly', () => { - let semver = new SemVersion(version); + const semver = new SemVersion(version); expect(semver.major).toBe(1); expect(semver.minor).toBe(0); expect(semver.patch).toBe(0); @@ -19,15 +19,15 @@ describe("SemVersion", () => { }); it('should detect greater version properly', () => { - let semver = new SemVersion(version); - let cases = [ - {value: '3.4.5', expected: true}, - {value: '3.4.4', expected: true}, - {value: '3.4.6', expected: false}, - {value: '4', expected: false}, - {value: '3.5', expected: false}, + const semver = new SemVersion(version); + const cases = [ + { value: '3.4.5', expected: true }, + { value: '3.4.4', expected: true }, + { value: '3.4.6', expected: false }, + { value: '4', expected: false }, + { value: '3.5', expected: false }, ]; - cases.forEach((testCase) => { + cases.forEach(testCase => { expect(semver.isGtOrEq(testCase.value)).toBe(testCase.expected); }); }); @@ -35,17 +35,17 @@ describe("SemVersion", () => { describe('isVersionGtOrEq', () => { it('should compare versions properly (a >= b)', () => { - let cases = [ - {values: ['3.4.5', '3.4.5'], expected: true}, - {values: ['3.4.5', '3.4.4'] , expected: true}, - {values: ['3.4.5', '3.4.6'], expected: false}, - {values: ['3.4', '3.4.0'], expected: true}, - {values: ['3', '3.0.0'], expected: true}, - {values: ['3.1.1-beta1', '3.1'], expected: true}, - {values: ['3.4.5', '4'], expected: false}, - {values: ['3.4.5', '3.5'], expected: false}, + const cases = [ + { values: ['3.4.5', '3.4.5'], expected: true }, + { values: ['3.4.5', '3.4.4'], expected: true }, + { values: ['3.4.5', '3.4.6'], expected: false }, + { values: ['3.4', '3.4.0'], expected: true }, + { values: ['3', '3.0.0'], expected: true }, + { values: ['3.1.1-beta1', '3.1'], expected: true }, + { values: ['3.4.5', '4'], expected: false }, + { values: ['3.4.5', '3.5'], expected: false }, ]; - cases.forEach((testCase) => { + cases.forEach(testCase => { expect(isVersionGtOrEq(testCase.values[0], testCase.values[1])).toBe(testCase.expected); }); }); diff --git a/public/test/index.ts b/public/test/index.ts index 33f24331b67..05a47686775 100644 --- a/public/test/index.ts +++ b/public/test/index.ts @@ -22,10 +22,6 @@ angular.module('grafana.filters', []); angular.module('grafana.routes', ['ngRoute']); const context = (require).context('../', true, /specs\.(tsx?|js)/); -for (let key of context.keys()) { +for (const key of context.keys()) { context(key); } - - - - diff --git a/public/test/mocks/common.ts b/public/test/mocks/common.ts index 1531f2ed176..64d12fdf725 100644 --- a/public/test/mocks/common.ts +++ b/public/test/mocks/common.ts @@ -7,10 +7,10 @@ export const backendSrv = { }; export function createNavTree(...args) { - let root = []; + const root = []; let node = root; - for (let arg of args) { - let child = { id: arg, url: `/url/${arg}`, text: `${arg}-Text`, children: [] }; + for (const arg of args) { + const child = { id: arg, url: `/url/${arg}`, text: `${arg}-Text`, children: [] }; node.push(child); node = child.children; } From 314b645857bf82f6fef66e9d4c1af18dd6854567 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Sun, 26 Aug 2018 18:43:07 +0200 Subject: [PATCH 508/786] tslint: changing vars -> const (#13034) --- .../alerting/specs/threshold_mapper.test.ts | 12 +- .../dashboard/specs/dashboard_model.test.ts | 48 +++---- .../features/dashboard/specs/exporter.test.ts | 30 ++--- .../dashboard/specs/save_as_modal.test.ts | 12 +- .../specs/save_provisioned_modal.test.ts | 6 +- .../dashboard/specs/share_modal_ctrl.test.ts | 10 +- .../features/dashboard/specs/time_srv.test.ts | 22 ++-- .../dashboard/specs/viewstate_srv.test.ts | 2 +- .../panellinks/specs/link_srv.test.ts | 18 +-- .../templating/specs/adhoc_variable.test.ts | 6 +- .../templating/specs/query_variable.test.ts | 14 +-- .../templating/specs/template_srv.test.ts | 100 +++++++-------- .../templating/specs/variable.test.ts | 22 ++-- .../templating/specs/variable_srv.test.ts | 26 ++-- .../specs/variable_srv_init.test.ts | 16 +-- .../cloudwatch/specs/datasource.test.ts | 32 ++--- .../elasticsearch/specs/datasource.test.ts | 24 ++-- .../elasticsearch/specs/index_pattern.test.ts | 18 +-- .../elasticsearch/specs/query_builder.test.ts | 52 ++++---- .../elasticsearch/specs/query_def.test.ts | 18 +-- .../datasource/grafana-live/datasource.ts | 6 +- .../graphite/specs/datasource.test.ts | 2 +- .../datasource/graphite/specs/gfunc.test.ts | 42 +++---- .../datasource/graphite/specs/lexer.test.ts | 56 ++++----- .../datasource/graphite/specs/parser.test.ts | 80 ++++++------ .../influxdb/specs/influx_query.test.ts | 66 +++++----- .../influxdb/specs/influx_series.test.ts | 64 +++++----- .../influxdb/specs/query_builder.test.ts | 68 +++++----- .../influxdb/specs/query_part.test.ts | 32 ++--- .../influxdb/specs/response_parser.test.ts | 32 ++--- .../opentsdb/specs/datasource.test.ts | 4 +- .../opentsdb/specs/query_ctrl.test.ts | 2 +- .../prometheus/specs/datasource.test.ts | 118 +++++++++--------- .../specs/result_transformer.test.ts | 12 +- .../panel/graph/specs/data_processor.test.ts | 10 +- .../plugins/panel/graph/specs/graph.test.ts | 16 +-- .../panel/graph/specs/graph_ctrl.test.ts | 8 +- .../panel/graph/specs/graph_tooltip.test.ts | 24 ++-- .../graph/specs/threshold_manager.test.ts | 28 ++--- .../panel/heatmap/specs/heatmap_ctrl.test.ts | 8 +- .../singlestat/specs/singlestat_panel.test.ts | 6 +- .../panel/table/specs/renderer.test.ts | 76 +++++------ .../panel/table/specs/transformers.test.ts | 40 +++--- public/test/jest-setup.ts | 2 +- public/test/specs/helpers.ts | 10 +- 45 files changed, 650 insertions(+), 650 deletions(-) diff --git a/public/app/features/alerting/specs/threshold_mapper.test.ts b/public/app/features/alerting/specs/threshold_mapper.test.ts index b9fa45a6e49..922d9c8787e 100644 --- a/public/app/features/alerting/specs/threshold_mapper.test.ts +++ b/public/app/features/alerting/specs/threshold_mapper.test.ts @@ -5,7 +5,7 @@ import { ThresholdMapper } from '../threshold_mapper'; describe('ThresholdMapper', () => { describe('with greater than evaluator', () => { it('can map query conditions to thresholds', () => { - var panel: any = { + const panel: any = { type: 'graph', alert: { conditions: [ @@ -17,7 +17,7 @@ describe('ThresholdMapper', () => { }, }; - var updated = ThresholdMapper.alertToGraphThresholds(panel); + const updated = ThresholdMapper.alertToGraphThresholds(panel); expect(updated).toBe(true); expect(panel.thresholds[0].op).toBe('gt'); expect(panel.thresholds[0].value).toBe(100); @@ -26,7 +26,7 @@ describe('ThresholdMapper', () => { describe('with outside range evaluator', () => { it('can map query conditions to thresholds', () => { - var panel: any = { + const panel: any = { type: 'graph', alert: { conditions: [ @@ -38,7 +38,7 @@ describe('ThresholdMapper', () => { }, }; - var updated = ThresholdMapper.alertToGraphThresholds(panel); + const updated = ThresholdMapper.alertToGraphThresholds(panel); expect(updated).toBe(true); expect(panel.thresholds[0].op).toBe('lt'); expect(panel.thresholds[0].value).toBe(100); @@ -50,7 +50,7 @@ describe('ThresholdMapper', () => { describe('with inside range evaluator', () => { it('can map query conditions to thresholds', () => { - var panel: any = { + const panel: any = { type: 'graph', alert: { conditions: [ @@ -62,7 +62,7 @@ describe('ThresholdMapper', () => { }, }; - var updated = ThresholdMapper.alertToGraphThresholds(panel); + const updated = ThresholdMapper.alertToGraphThresholds(panel); expect(updated).toBe(true); expect(panel.thresholds[0].op).toBe('gt'); expect(panel.thresholds[0].value).toBe(100); diff --git a/public/app/features/dashboard/specs/dashboard_model.test.ts b/public/app/features/dashboard/specs/dashboard_model.test.ts index 28029653a6c..24d036a8233 100644 --- a/public/app/features/dashboard/specs/dashboard_model.test.ts +++ b/public/app/features/dashboard/specs/dashboard_model.test.ts @@ -6,7 +6,7 @@ jest.mock('app/core/services/context_srv', () => ({})); describe('DashboardModel', function() { describe('when creating new dashboard model defaults only', function() { - var model; + let model; beforeEach(function() { model = new DashboardModel({}, {}); @@ -27,7 +27,7 @@ describe('DashboardModel', function() { }); describe('when getting next panel id', function() { - var model; + let model; beforeEach(function() { model = new DashboardModel({ @@ -42,16 +42,16 @@ describe('DashboardModel', function() { describe('getSaveModelClone', function() { it('should sort keys', () => { - var model = new DashboardModel({}); - var saveModel = model.getSaveModelClone(); - var keys = _.keys(saveModel); + const model = new DashboardModel({}); + const saveModel = model.getSaveModelClone(); + const keys = _.keys(saveModel); expect(keys[0]).toBe('annotations'); expect(keys[1]).toBe('autoUpdate'); }); it('should remove add panel panels', () => { - var model = new DashboardModel({}); + const model = new DashboardModel({}); model.addPanel({ type: 'add-panel', }); @@ -61,15 +61,15 @@ describe('DashboardModel', function() { model.addPanel({ type: 'add-panel', }); - var saveModel = model.getSaveModelClone(); - var panels = saveModel.panels; + const saveModel = model.getSaveModelClone(); + const panels = saveModel.panels; expect(panels.length).toBe(1); }); }); describe('row and panel manipulation', function() { - var dashboard; + let dashboard; beforeEach(function() { dashboard = new DashboardModel({}); @@ -82,7 +82,7 @@ describe('DashboardModel', function() { }); it('duplicate panel should try to add to the right if there is space', function() { - var panel = { id: 10, gridPos: { x: 0, y: 0, w: 6, h: 2 } }; + const panel = { id: 10, gridPos: { x: 0, y: 0, w: 6, h: 2 } }; dashboard.addPanel(panel); dashboard.duplicatePanel(dashboard.panels[0]); @@ -96,7 +96,7 @@ describe('DashboardModel', function() { }); it('duplicate panel should remove repeat data', function() { - var panel = { + const panel = { id: 10, gridPos: { x: 0, y: 0, w: 6, h: 2 }, repeat: 'asd', @@ -112,7 +112,7 @@ describe('DashboardModel', function() { }); describe('Given editable false dashboard', function() { - var model; + let model; beforeEach(function() { model = new DashboardModel({ editable: false }); @@ -124,14 +124,14 @@ describe('DashboardModel', function() { }); it('getSaveModelClone should remove meta', function() { - var clone = model.getSaveModelClone(); + const clone = model.getSaveModelClone(); expect(clone.meta).toBe(undefined); }); }); describe('when loading dashboard with old influxdb query schema', function() { - var model; - var target; + let model; + let target; beforeEach(function() { model = new DashboardModel({ @@ -197,7 +197,7 @@ describe('DashboardModel', function() { }); describe('when creating dashboard model with missing list for annoations or templating', function() { - var model; + let model; beforeEach(function() { model = new DashboardModel({ @@ -222,7 +222,7 @@ describe('DashboardModel', function() { }); describe('Formatting epoch timestamp when timezone is set as utc', function() { - var dashboard; + let dashboard; beforeEach(function() { dashboard = new DashboardModel({ timezone: 'utc' }); @@ -242,7 +242,7 @@ describe('DashboardModel', function() { }); describe('updateSubmenuVisibility with empty lists', function() { - var model; + let model; beforeEach(function() { model = new DashboardModel({}); @@ -255,7 +255,7 @@ describe('DashboardModel', function() { }); describe('updateSubmenuVisibility with annotation', function() { - var model; + let model; beforeEach(function() { model = new DashboardModel({ @@ -272,7 +272,7 @@ describe('DashboardModel', function() { }); describe('updateSubmenuVisibility with template var', function() { - var model; + let model; beforeEach(function() { model = new DashboardModel({ @@ -289,7 +289,7 @@ describe('DashboardModel', function() { }); describe('updateSubmenuVisibility with hidden template var', function() { - var model; + let model; beforeEach(function() { model = new DashboardModel({ @@ -306,7 +306,7 @@ describe('DashboardModel', function() { }); describe('updateSubmenuVisibility with hidden annotation toggle', function() { - var dashboard; + let dashboard; beforeEach(function() { dashboard = new DashboardModel({ @@ -323,7 +323,7 @@ describe('DashboardModel', function() { }); describe('When collapsing row', function() { - var dashboard; + let dashboard; beforeEach(function() { dashboard = new DashboardModel({ @@ -365,7 +365,7 @@ describe('DashboardModel', function() { }); describe('When expanding row', function() { - var dashboard; + let dashboard; beforeEach(function() { dashboard = new DashboardModel({ diff --git a/public/app/features/dashboard/specs/exporter.test.ts b/public/app/features/dashboard/specs/exporter.test.ts index c7727a4af4d..c7a232f925b 100644 --- a/public/app/features/dashboard/specs/exporter.test.ts +++ b/public/app/features/dashboard/specs/exporter.test.ts @@ -10,7 +10,7 @@ import { DashboardExporter } from '../export/exporter'; import { DashboardModel } from '../dashboard_model'; describe('given dashboard with repeated panels', () => { - var dash, exported; + let dash, exported; beforeEach(done => { dash = { @@ -89,7 +89,7 @@ describe('given dashboard with repeated panels', () => { config.buildInfo.version = '3.0.2'; //Stubs test function calls - var datasourceSrvStub = { get: jest.fn(arg => getStub(arg)) }; + const datasourceSrvStub = { get: jest.fn(arg => getStub(arg)) }; config.panels['graph'] = { id: 'graph', @@ -110,7 +110,7 @@ describe('given dashboard with repeated panels', () => { }; dash = new DashboardModel(dash, {}); - var exporter = new DashboardExporter(datasourceSrvStub); + const exporter = new DashboardExporter(datasourceSrvStub); exporter.makeExportable(dash).then(clean => { exported = clean; done(); @@ -118,12 +118,12 @@ describe('given dashboard with repeated panels', () => { }); it('should replace datasource refs', () => { - var panel = exported.panels[0]; + const panel = exported.panels[0]; expect(panel.datasource).toBe('${DS_GFDB}'); }); it('should replace datasource refs in collapsed row', () => { - var panel = exported.panels[5].panels[0]; + const panel = exported.panels[5].panels[0]; expect(panel.datasource).toBe('${DS_GFDB}'); }); @@ -145,7 +145,7 @@ describe('given dashboard with repeated panels', () => { }); it('should add datasource to required', () => { - var require = _.find(exported.__requires, { name: 'TestDB' }); + const require = _.find(exported.__requires, { name: 'TestDB' }); expect(require.name).toBe('TestDB'); expect(require.id).toBe('testdb'); expect(require.type).toBe('datasource'); @@ -153,52 +153,52 @@ describe('given dashboard with repeated panels', () => { }); it('should not add built in datasources to required', () => { - var require = _.find(exported.__requires, { name: 'Mixed' }); + const require = _.find(exported.__requires, { name: 'Mixed' }); expect(require).toBe(undefined); }); it('should add datasources used in mixed mode', () => { - var require = _.find(exported.__requires, { name: 'OtherDB' }); + const require = _.find(exported.__requires, { name: 'OtherDB' }); expect(require).not.toBe(undefined); }); it('should add graph panel to required', () => { - var require = _.find(exported.__requires, { name: 'Graph' }); + const require = _.find(exported.__requires, { name: 'Graph' }); expect(require.name).toBe('Graph'); expect(require.id).toBe('graph'); expect(require.version).toBe('1.1.0'); }); it('should add table panel to required', () => { - var require = _.find(exported.__requires, { name: 'Table' }); + const require = _.find(exported.__requires, { name: 'Table' }); expect(require.name).toBe('Table'); expect(require.id).toBe('table'); expect(require.version).toBe('1.1.1'); }); it('should add heatmap panel to required', () => { - var require = _.find(exported.__requires, { name: 'Heatmap' }); + const require = _.find(exported.__requires, { name: 'Heatmap' }); expect(require.name).toBe('Heatmap'); expect(require.id).toBe('heatmap'); expect(require.version).toBe('1.1.2'); }); it('should add grafana version', () => { - var require = _.find(exported.__requires, { name: 'Grafana' }); + const require = _.find(exported.__requires, { name: 'Grafana' }); expect(require.type).toBe('grafana'); expect(require.id).toBe('grafana'); expect(require.version).toBe('3.0.2'); }); it('should add constant template variables as inputs', () => { - var input = _.find(exported.__inputs, { name: 'VAR_PREFIX' }); + const input = _.find(exported.__inputs, { name: 'VAR_PREFIX' }); expect(input.type).toBe('constant'); expect(input.label).toBe('prefix'); expect(input.value).toBe('collectd'); }); it('should templatize constant variables', () => { - var variable = _.find(exported.templating.list, { name: 'prefix' }); + const variable = _.find(exported.templating.list, { name: 'prefix' }); expect(variable.query).toBe('${VAR_PREFIX}'); expect(variable.current.text).toBe('${VAR_PREFIX}'); expect(variable.current.value).toBe('${VAR_PREFIX}'); @@ -208,7 +208,7 @@ describe('given dashboard with repeated panels', () => { }); // Stub responses -var stubs = []; +const stubs = []; stubs['gfdb'] = { name: 'gfdb', meta: { id: 'testdb', info: { version: '1.2.1' }, name: 'TestDB' }, diff --git a/public/app/features/dashboard/specs/save_as_modal.test.ts b/public/app/features/dashboard/specs/save_as_modal.test.ts index bb16d1bcc1c..29ed694474b 100644 --- a/public/app/features/dashboard/specs/save_as_modal.test.ts +++ b/public/app/features/dashboard/specs/save_as_modal.test.ts @@ -4,12 +4,12 @@ import { describe, it, expect } from 'test/lib/common'; describe('saving dashboard as', () => { function scenario(name, panel, verify) { describe(name, () => { - var json = { + const json = { title: 'name', panels: [panel], }; - var mockDashboardSrv = { + const mockDashboardSrv = { getCurrent: function() { return { id: 5, @@ -21,8 +21,8 @@ describe('saving dashboard as', () => { }, }; - var ctrl = new SaveDashboardAsModalCtrl(mockDashboardSrv); - var ctx: any = { + const ctrl = new SaveDashboardAsModalCtrl(mockDashboardSrv); + const ctx: any = { clone: ctrl.clone, ctrl: ctrl, panel: panel, @@ -35,14 +35,14 @@ describe('saving dashboard as', () => { } scenario('default values', {}, ctx => { - var clone = ctx.clone; + const clone = ctx.clone; expect(clone.id).toBe(null); expect(clone.title).toBe('name Copy'); expect(clone.editable).toBe(true); expect(clone.hideControls).toBe(false); }); - var graphPanel = { + const graphPanel = { id: 1, type: 'graph', alert: { rule: 1 }, diff --git a/public/app/features/dashboard/specs/save_provisioned_modal.test.ts b/public/app/features/dashboard/specs/save_provisioned_modal.test.ts index ce921cee8c8..fb1a652a03c 100644 --- a/public/app/features/dashboard/specs/save_provisioned_modal.test.ts +++ b/public/app/features/dashboard/specs/save_provisioned_modal.test.ts @@ -1,12 +1,12 @@ import { SaveProvisionedDashboardModalCtrl } from '../save_provisioned_modal'; describe('SaveProvisionedDashboardModalCtrl', () => { - var json = { + const json = { title: 'name', id: 5, }; - var mockDashboardSrv = { + const mockDashboardSrv = { getCurrent: function() { return { id: 5, @@ -18,7 +18,7 @@ describe('SaveProvisionedDashboardModalCtrl', () => { }, }; - var ctrl = new SaveProvisionedDashboardModalCtrl(mockDashboardSrv); + const ctrl = new SaveProvisionedDashboardModalCtrl(mockDashboardSrv); it('should remove id from dashboard model', () => { expect(ctrl.dash.id).toBeUndefined(); diff --git a/public/app/features/dashboard/specs/share_modal_ctrl.test.ts b/public/app/features/dashboard/specs/share_modal_ctrl.test.ts index 35261256566..796baf7f522 100644 --- a/public/app/features/dashboard/specs/share_modal_ctrl.test.ts +++ b/public/app/features/dashboard/specs/share_modal_ctrl.test.ts @@ -4,7 +4,7 @@ import config from 'app/core/config'; import { LinkSrv } from 'app/features/panellinks/link_srv'; describe('ShareModalCtrl', () => { - var ctx = { + const ctx = { timeSrv: { timeRange: () => { return { from: new Date(1000), to: new Date(2000) }; @@ -68,8 +68,8 @@ describe('ShareModalCtrl', () => { ctx.scope.panel = { id: 22 }; ctx.scope.init(); - var base = 'http://dashboards.grafana.com/render/d-solo/abcdefghi/my-dash'; - var params = '?from=1000&to=2000&orgId=1&panelId=22&width=1000&height=500&tz=UTC'; + const base = 'http://dashboards.grafana.com/render/d-solo/abcdefghi/my-dash'; + const params = '?from=1000&to=2000&orgId=1&panelId=22&width=1000&height=500&tz=UTC'; expect(ctx.scope.imageUrl).toContain(base + params); }); @@ -79,8 +79,8 @@ describe('ShareModalCtrl', () => { ctx.scope.panel = { id: 22 }; ctx.scope.init(); - var base = 'http://dashboards.grafana.com/render/dashboard-solo/script/my-dash.js'; - var params = '?from=1000&to=2000&orgId=1&panelId=22&width=1000&height=500&tz=UTC'; + const base = 'http://dashboards.grafana.com/render/dashboard-solo/script/my-dash.js'; + const params = '?from=1000&to=2000&orgId=1&panelId=22&width=1000&height=500&tz=UTC'; expect(ctx.scope.imageUrl).toContain(base + params); }); diff --git a/public/app/features/dashboard/specs/time_srv.test.ts b/public/app/features/dashboard/specs/time_srv.test.ts index f8d9e42cfd4..046ac52c9bf 100644 --- a/public/app/features/dashboard/specs/time_srv.test.ts +++ b/public/app/features/dashboard/specs/time_srv.test.ts @@ -3,25 +3,25 @@ import '../time_srv'; import moment from 'moment'; describe('timeSrv', function() { - var rootScope = { + const rootScope = { $on: jest.fn(), onAppEvent: jest.fn(), appEvent: jest.fn(), }; - var timer = { + const timer = { register: jest.fn(), cancel: jest.fn(), cancelAll: jest.fn(), }; - var location = { + let location = { search: jest.fn(() => ({})), }; - var timeSrv; + let timeSrv; - var _dashboard: any = { + const _dashboard: any = { time: { from: 'now-6h', to: 'now' }, getTimezone: jest.fn(() => 'browser'), }; @@ -34,14 +34,14 @@ describe('timeSrv', function() { describe('timeRange', function() { it('should return unparsed when parse is false', function() { timeSrv.setTime({ from: 'now', to: 'now-1h' }); - var time = timeSrv.timeRange(); + const time = timeSrv.timeRange(); expect(time.raw.from).toBe('now'); expect(time.raw.to).toBe('now-1h'); }); it('should return parsed when parse is true', function() { timeSrv.setTime({ from: 'now', to: 'now-1h' }); - var time = timeSrv.timeRange(); + const time = timeSrv.timeRange(); expect(moment.isMoment(time.from)).toBe(true); expect(moment.isMoment(time.to)).toBe(true); }); @@ -58,7 +58,7 @@ describe('timeSrv', function() { timeSrv = new TimeSrv(rootScope, jest.fn(), location, timer, { isGrafanaVisibile: jest.fn() }); timeSrv.init(_dashboard); - var time = timeSrv.timeRange(); + const time = timeSrv.timeRange(); expect(time.raw.from).toBe('now-2d'); expect(time.raw.to).toBe('now'); }); @@ -74,7 +74,7 @@ describe('timeSrv', function() { timeSrv = new TimeSrv(rootScope, jest.fn(), location, timer, { isGrafanaVisibile: jest.fn() }); timeSrv.init(_dashboard); - var time = timeSrv.timeRange(); + const time = timeSrv.timeRange(); expect(time.from.valueOf()).toEqual(new Date('2014-04-10T05:20:10Z').getTime()); expect(time.to.valueOf()).toEqual(new Date('2014-05-20T03:10:22Z').getTime()); }); @@ -90,7 +90,7 @@ describe('timeSrv', function() { timeSrv = new TimeSrv(rootScope, jest.fn(), location, timer, { isGrafanaVisibile: jest.fn() }); timeSrv.init(_dashboard); - var time = timeSrv.timeRange(); + const time = timeSrv.timeRange(); expect(time.from.valueOf()).toEqual(new Date('2014-04-10T00:00:00Z').getTime()); expect(time.to.valueOf()).toEqual(new Date('2014-05-20T00:00:00Z').getTime()); }); @@ -106,7 +106,7 @@ describe('timeSrv', function() { timeSrv = new TimeSrv(rootScope, jest.fn(), location, timer, { isGrafanaVisibile: jest.fn() }); timeSrv.init(_dashboard); - var time = timeSrv.timeRange(); + const time = timeSrv.timeRange(); expect(time.from.valueOf()).toEqual(1410337646373); expect(time.to.valueOf()).toEqual(1410337665699); }); diff --git a/public/app/features/dashboard/specs/viewstate_srv.test.ts b/public/app/features/dashboard/specs/viewstate_srv.test.ts index 740e3c3b9a8..905ffb8b355 100644 --- a/public/app/features/dashboard/specs/viewstate_srv.test.ts +++ b/public/app/features/dashboard/specs/viewstate_srv.test.ts @@ -37,7 +37,7 @@ describe('when updating view state', () => { }); it('should update querystring and view state', () => { - var updateState = { fullscreen: true, edit: true, panelId: 1 }; + const updateState = { fullscreen: true, edit: true, panelId: 1 }; viewState.update(updateState); diff --git a/public/app/features/panellinks/specs/link_srv.test.ts b/public/app/features/panellinks/specs/link_srv.test.ts index 521a4edef15..9c6b62d4b69 100644 --- a/public/app/features/panellinks/specs/link_srv.test.ts +++ b/public/app/features/panellinks/specs/link_srv.test.ts @@ -7,9 +7,9 @@ jest.mock('angular', () => { }); describe('linkSrv', function() { - var linkSrv; - var templateSrvMock = {}; - var timeSrvMock = {}; + let linkSrv; + const templateSrvMock = {}; + const timeSrvMock = {}; beforeEach(() => { linkSrv = new LinkSrv(templateSrvMock, timeSrvMock); @@ -17,29 +17,29 @@ describe('linkSrv', function() { describe('when appending query strings', function() { it('add ? to URL if not present', function() { - var url = linkSrv.appendToQueryString('http://example.com', 'foo=bar'); + const url = linkSrv.appendToQueryString('http://example.com', 'foo=bar'); expect(url).toBe('http://example.com?foo=bar'); }); it('do not add & to URL if ? is present but query string is empty', function() { - var url = linkSrv.appendToQueryString('http://example.com?', 'foo=bar'); + const url = linkSrv.appendToQueryString('http://example.com?', 'foo=bar'); expect(url).toBe('http://example.com?foo=bar'); }); it('add & to URL if query string is present', function() { - var url = linkSrv.appendToQueryString('http://example.com?foo=bar', 'hello=world'); + const url = linkSrv.appendToQueryString('http://example.com?foo=bar', 'hello=world'); expect(url).toBe('http://example.com?foo=bar&hello=world'); }); it('do not change the URL if there is nothing to append', function() { _.each(['', undefined, null], function(toAppend) { - var url1 = linkSrv.appendToQueryString('http://example.com', toAppend); + const url1 = linkSrv.appendToQueryString('http://example.com', toAppend); expect(url1).toBe('http://example.com'); - var url2 = linkSrv.appendToQueryString('http://example.com?', toAppend); + const url2 = linkSrv.appendToQueryString('http://example.com?', toAppend); expect(url2).toBe('http://example.com?'); - var url3 = linkSrv.appendToQueryString('http://example.com?foo=bar', toAppend); + const url3 = linkSrv.appendToQueryString('http://example.com?foo=bar', toAppend); expect(url3).toBe('http://example.com?foo=bar'); }); }); diff --git a/public/app/features/templating/specs/adhoc_variable.test.ts b/public/app/features/templating/specs/adhoc_variable.test.ts index a7b20e8d029..f85c49e73d5 100644 --- a/public/app/features/templating/specs/adhoc_variable.test.ts +++ b/public/app/features/templating/specs/adhoc_variable.test.ts @@ -3,21 +3,21 @@ import { AdhocVariable } from '../adhoc_variable'; describe('AdhocVariable', function() { describe('when serializing to url', function() { it('should set return key value and op separated by pipe', function() { - var variable = new AdhocVariable({ + const variable = new AdhocVariable({ filters: [ { key: 'key1', operator: '=', value: 'value1' }, { key: 'key2', operator: '!=', value: 'value2' }, { key: 'key3', operator: '=', value: 'value3a|value3b|value3c' }, ], }); - var urlValue = variable.getValueForUrl(); + const urlValue = variable.getValueForUrl(); expect(urlValue).toMatchObject(['key1|=|value1', 'key2|!=|value2', 'key3|=|value3a__gfp__value3b__gfp__value3c']); }); }); describe('when deserializing from url', function() { it('should restore filters', function() { - var variable = new AdhocVariable({}); + const variable = new AdhocVariable({}); variable.setValueFromUrl(['key1|=|value1', 'key2|!=|value2', 'key3|=|value3a__gfp__value3b__gfp__value3c']); expect(variable.filters[0].key).toBe('key1'); diff --git a/public/app/features/templating/specs/query_variable.test.ts b/public/app/features/templating/specs/query_variable.test.ts index 39c51874586..85a36702d3c 100644 --- a/public/app/features/templating/specs/query_variable.test.ts +++ b/public/app/features/templating/specs/query_variable.test.ts @@ -3,7 +3,7 @@ import { QueryVariable } from '../query_variable'; describe('QueryVariable', () => { describe('when creating from model', () => { it('should set defaults', () => { - var variable = new QueryVariable({}, null, null, null, null); + const variable = new QueryVariable({}, null, null, null, null); expect(variable.datasource).toBe(null); expect(variable.refresh).toBe(0); expect(variable.sort).toBe(0); @@ -15,13 +15,13 @@ describe('QueryVariable', () => { }); it('get model should copy changes back to model', () => { - var variable = new QueryVariable({}, null, null, null, null); + const variable = new QueryVariable({}, null, null, null, null); variable.options = [{ text: 'test' }]; variable.datasource = 'google'; variable.regex = 'asd'; variable.sort = 50; - var model = variable.getSaveModel(); + const model = variable.getSaveModel(); expect(model.options.length).toBe(1); expect(model.options[0].text).toBe('test'); expect(model.datasource).toBe('google'); @@ -30,11 +30,11 @@ describe('QueryVariable', () => { }); it('if refresh != 0 then remove options in presisted mode', () => { - var variable = new QueryVariable({}, null, null, null, null); + const variable = new QueryVariable({}, null, null, null, null); variable.options = [{ text: 'test' }]; variable.refresh = 1; - var model = variable.getSaveModel(); + const model = variable.getSaveModel(); expect(model.options.length).toBe(0); }); }); @@ -69,7 +69,7 @@ describe('QueryVariable', () => { }); it('should return in same order', () => { - var i = 0; + let i = 0; expect(result.length).toBe(11); expect(result[i++].text).toBe(''); expect(result[i++].text).toBe('0'); @@ -90,7 +90,7 @@ describe('QueryVariable', () => { }); it('should return in same order', () => { - var i = 0; + let i = 0; expect(result.length).toBe(11); expect(result[i++].text).toBe(''); expect(result[i++].text).toBe('0'); diff --git a/public/app/features/templating/specs/template_srv.test.ts b/public/app/features/templating/specs/template_srv.test.ts index 86b6aa7ec99..984d62cb729 100644 --- a/public/app/features/templating/specs/template_srv.test.ts +++ b/public/app/features/templating/specs/template_srv.test.ts @@ -1,7 +1,7 @@ import { TemplateSrv } from '../template_srv'; describe('templateSrv', function() { - var _templateSrv; + let _templateSrv; function initTemplateSrv(variables) { _templateSrv = new TemplateSrv(); @@ -14,7 +14,7 @@ describe('templateSrv', function() { }); it('should initialize template data', function() { - var target = _templateSrv.replace('this.[[test]].filters'); + const target = _templateSrv.replace('this.[[test]].filters'); expect(target).toBe('this.oogle.filters'); }); }); @@ -25,42 +25,42 @@ describe('templateSrv', function() { }); it('should replace $test with scoped value', function() { - var target = _templateSrv.replace('this.$test.filters', { + const target = _templateSrv.replace('this.$test.filters', { test: { value: 'mupp', text: 'asd' }, }); expect(target).toBe('this.mupp.filters'); }); it('should replace ${test} with scoped value', function() { - var target = _templateSrv.replace('this.${test}.filters', { + const target = _templateSrv.replace('this.${test}.filters', { test: { value: 'mupp', text: 'asd' }, }); expect(target).toBe('this.mupp.filters'); }); it('should replace ${test:glob} with scoped value', function() { - var target = _templateSrv.replace('this.${test:glob}.filters', { + const target = _templateSrv.replace('this.${test:glob}.filters', { test: { value: 'mupp', text: 'asd' }, }); expect(target).toBe('this.mupp.filters'); }); it('should replace $test with scoped text', function() { - var target = _templateSrv.replaceWithText('this.$test.filters', { + const target = _templateSrv.replaceWithText('this.$test.filters', { test: { value: 'mupp', text: 'asd' }, }); expect(target).toBe('this.asd.filters'); }); it('should replace ${test} with scoped text', function() { - var target = _templateSrv.replaceWithText('this.${test}.filters', { + const target = _templateSrv.replaceWithText('this.${test}.filters', { test: { value: 'mupp', text: 'asd' }, }); expect(target).toBe('this.asd.filters'); }); it('should replace ${test:glob} with scoped text', function() { - var target = _templateSrv.replaceWithText('this.${test:glob}.filters', { + const target = _templateSrv.replaceWithText('this.${test:glob}.filters', { test: { value: 'mupp', text: 'asd' }, }); expect(target).toBe('this.asd.filters'); @@ -81,17 +81,17 @@ describe('templateSrv', function() { }); it('should return filters if datasourceName match', function() { - var filters = _templateSrv.getAdhocFilters('oogle'); + const filters = _templateSrv.getAdhocFilters('oogle'); expect(filters).toMatchObject([1]); }); it('should return empty array if datasourceName does not match', function() { - var filters = _templateSrv.getAdhocFilters('oogleasdasd'); + const filters = _templateSrv.getAdhocFilters('oogleasdasd'); expect(filters).toMatchObject([]); }); it('should return filters when datasourceName match via data source variable', function() { - var filters = _templateSrv.getAdhocFilters('logstash'); + const filters = _templateSrv.getAdhocFilters('logstash'); expect(filters).toMatchObject([2]); }); }); @@ -108,37 +108,37 @@ describe('templateSrv', function() { }); it('should replace $test with globbed value', function() { - var target = _templateSrv.replace('this.$test.filters', {}, 'glob'); + const target = _templateSrv.replace('this.$test.filters', {}, 'glob'); expect(target).toBe('this.{value1,value2}.filters'); }); it('should replace ${test} with globbed value', function() { - var target = _templateSrv.replace('this.${test}.filters', {}, 'glob'); + const target = _templateSrv.replace('this.${test}.filters', {}, 'glob'); expect(target).toBe('this.{value1,value2}.filters'); }); it('should replace ${test:glob} with globbed value', function() { - var target = _templateSrv.replace('this.${test:glob}.filters', {}); + const target = _templateSrv.replace('this.${test:glob}.filters', {}); expect(target).toBe('this.{value1,value2}.filters'); }); it('should replace $test with piped value', function() { - var target = _templateSrv.replace('this=$test', {}, 'pipe'); + const target = _templateSrv.replace('this=$test', {}, 'pipe'); expect(target).toBe('this=value1|value2'); }); it('should replace ${test} with piped value', function() { - var target = _templateSrv.replace('this=${test}', {}, 'pipe'); + const target = _templateSrv.replace('this=${test}', {}, 'pipe'); expect(target).toBe('this=value1|value2'); }); it('should replace ${test:pipe} with piped value', function() { - var target = _templateSrv.replace('this=${test:pipe}', {}); + const target = _templateSrv.replace('this=${test:pipe}', {}); expect(target).toBe('this=value1|value2'); }); it('should replace ${test:pipe} with piped value and $test with globbed value', function() { - var target = _templateSrv.replace('${test:pipe},$test', {}, 'glob'); + const target = _templateSrv.replace('${test:pipe},$test', {}, 'glob'); expect(target).toBe('value1|value2,{value1,value2}'); }); }); @@ -156,22 +156,22 @@ describe('templateSrv', function() { }); it('should replace $test with formatted all value', function() { - var target = _templateSrv.replace('this.$test.filters', {}, 'glob'); + const target = _templateSrv.replace('this.$test.filters', {}, 'glob'); expect(target).toBe('this.{value1,value2}.filters'); }); it('should replace ${test} with formatted all value', function() { - var target = _templateSrv.replace('this.${test}.filters', {}, 'glob'); + const target = _templateSrv.replace('this.${test}.filters', {}, 'glob'); expect(target).toBe('this.{value1,value2}.filters'); }); it('should replace ${test:glob} with formatted all value', function() { - var target = _templateSrv.replace('this.${test:glob}.filters', {}); + const target = _templateSrv.replace('this.${test:glob}.filters', {}); expect(target).toBe('this.{value1,value2}.filters'); }); it('should replace ${test:pipe} with piped value and $test with globbed value', function() { - var target = _templateSrv.replace('${test:pipe},$test', {}, 'glob'); + const target = _templateSrv.replace('${test:pipe},$test', {}, 'glob'); expect(target).toBe('value1|value2,{value1,value2}'); }); }); @@ -190,22 +190,22 @@ describe('templateSrv', function() { }); it('should replace $test with formatted all value', function() { - var target = _templateSrv.replace('this.$test.filters', {}, 'glob'); + const target = _templateSrv.replace('this.$test.filters', {}, 'glob'); expect(target).toBe('this.*.filters'); }); it('should replace ${test} with formatted all value', function() { - var target = _templateSrv.replace('this.${test}.filters', {}, 'glob'); + const target = _templateSrv.replace('this.${test}.filters', {}, 'glob'); expect(target).toBe('this.*.filters'); }); it('should replace ${test:glob} with formatted all value', function() { - var target = _templateSrv.replace('this.${test:glob}.filters', {}); + const target = _templateSrv.replace('this.${test:glob}.filters', {}); expect(target).toBe('this.*.filters'); }); it('should not escape custom all value', function() { - var target = _templateSrv.replace('this.$test', {}, 'regex'); + const target = _templateSrv.replace('this.$test', {}, 'regex'); expect(target).toBe('this.*'); }); }); @@ -213,70 +213,70 @@ describe('templateSrv', function() { describe('lucene format', function() { it('should properly escape $test with lucene escape sequences', function() { initTemplateSrv([{ type: 'query', name: 'test', current: { value: 'value/4' } }]); - var target = _templateSrv.replace('this:$test', {}, 'lucene'); + const target = _templateSrv.replace('this:$test', {}, 'lucene'); expect(target).toBe('this:value\\/4'); }); it('should properly escape ${test} with lucene escape sequences', function() { initTemplateSrv([{ type: 'query', name: 'test', current: { value: 'value/4' } }]); - var target = _templateSrv.replace('this:${test}', {}, 'lucene'); + const target = _templateSrv.replace('this:${test}', {}, 'lucene'); expect(target).toBe('this:value\\/4'); }); it('should properly escape ${test:lucene} with lucene escape sequences', function() { initTemplateSrv([{ type: 'query', name: 'test', current: { value: 'value/4' } }]); - var target = _templateSrv.replace('this:${test:lucene}', {}); + const target = _templateSrv.replace('this:${test:lucene}', {}); expect(target).toBe('this:value\\/4'); }); }); describe('format variable to string values', function() { it('single value should return value', function() { - var result = _templateSrv.formatValue('test'); + const result = _templateSrv.formatValue('test'); expect(result).toBe('test'); }); it('multi value and glob format should render glob string', function() { - var result = _templateSrv.formatValue(['test', 'test2'], 'glob'); + const result = _templateSrv.formatValue(['test', 'test2'], 'glob'); expect(result).toBe('{test,test2}'); }); it('multi value and lucene should render as lucene expr', function() { - var result = _templateSrv.formatValue(['test', 'test2'], 'lucene'); + const result = _templateSrv.formatValue(['test', 'test2'], 'lucene'); expect(result).toBe('("test" OR "test2")'); }); it('multi value and regex format should render regex string', function() { - var result = _templateSrv.formatValue(['test.', 'test2'], 'regex'); + const result = _templateSrv.formatValue(['test.', 'test2'], 'regex'); expect(result).toBe('(test\\.|test2)'); }); it('multi value and pipe should render pipe string', function() { - var result = _templateSrv.formatValue(['test', 'test2'], 'pipe'); + const result = _templateSrv.formatValue(['test', 'test2'], 'pipe'); expect(result).toBe('test|test2'); }); it('multi value and distributed should render distributed string', function() { - var result = _templateSrv.formatValue(['test', 'test2'], 'distributed', { + const result = _templateSrv.formatValue(['test', 'test2'], 'distributed', { name: 'build', }); expect(result).toBe('test,build=test2'); }); it('multi value and distributed should render when not string', function() { - var result = _templateSrv.formatValue(['test'], 'distributed', { + const result = _templateSrv.formatValue(['test'], 'distributed', { name: 'build', }); expect(result).toBe('test'); }); it('multi value and csv format should render csv string', function() { - var result = _templateSrv.formatValue(['test', 'test2'], 'csv'); + const result = _templateSrv.formatValue(['test', 'test2'], 'csv'); expect(result).toBe('test,test2'); }); it('slash should be properly escaped in regex format', function() { - var result = _templateSrv.formatValue('Gi3/14', 'regex'); + const result = _templateSrv.formatValue('Gi3/14', 'regex'); expect(result).toBe('Gi3\\/14'); }); }); @@ -287,7 +287,7 @@ describe('templateSrv', function() { }); it('should return true if exists', function() { - var result = _templateSrv.variableExists('$test'); + const result = _templateSrv.variableExists('$test'); expect(result).toBe(true); }); }); @@ -298,17 +298,17 @@ describe('templateSrv', function() { }); it('should insert html', function() { - var result = _templateSrv.highlightVariablesAsHtml('$test'); + const result = _templateSrv.highlightVariablesAsHtml('$test'); expect(result).toBe('$test'); }); it('should insert html anywhere in string', function() { - var result = _templateSrv.highlightVariablesAsHtml('this $test ok'); + const result = _templateSrv.highlightVariablesAsHtml('this $test ok'); expect(result).toBe('this $test ok'); }); it('should ignore if variables does not exist', function() { - var result = _templateSrv.highlightVariablesAsHtml('this $google ok'); + const result = _templateSrv.highlightVariablesAsHtml('this $google ok'); expect(result).toBe('this $google ok'); }); }); @@ -319,7 +319,7 @@ describe('templateSrv', function() { }); it('should set current value and update template data', function() { - var target = _templateSrv.replace('this.[[test]].filters'); + const target = _templateSrv.replace('this.[[test]].filters'); expect(target).toBe('this.muuuu.filters'); }); }); @@ -339,7 +339,7 @@ describe('templateSrv', function() { }); it('should set multiple url params', function() { - var params = {}; + const params = {}; _templateSrv.fillVariableValuesForUrl(params); expect(params['var-test']).toMatchObject(['val1', 'val2']); }); @@ -360,7 +360,7 @@ describe('templateSrv', function() { }); it('should not include template variable value in url', function() { - var params = {}; + const params = {}; _templateSrv.fillVariableValuesForUrl(params); expect(params['var-test']).toBe(undefined); }); @@ -382,7 +382,7 @@ describe('templateSrv', function() { }); it('should not include template variable value in url', function() { - var params = {}; + const params = {}; _templateSrv.fillVariableValuesForUrl(params); expect(params['var-test']).toBe(undefined); }); @@ -394,7 +394,7 @@ describe('templateSrv', function() { }); it('should set scoped value as url params', function() { - var params = {}; + const params = {}; _templateSrv.fillVariableValuesForUrl(params, { test: { value: 'val1' }, }); @@ -408,7 +408,7 @@ describe('templateSrv', function() { }); it('should not set scoped value as url params', function() { - var params = {}; + const params = {}; _templateSrv.fillVariableValuesForUrl(params, { test: { name: 'test', value: 'val1', skipUrlSync: true }, }); @@ -435,7 +435,7 @@ describe('templateSrv', function() { }); it('should replace with text except for grafanaVariables', function() { - var target = _templateSrv.replaceWithText('Server: $server, period: $period'); + const target = _templateSrv.replaceWithText('Server: $server, period: $period'); expect(target).toBe('Server: All, period: 13m'); }); }); @@ -446,7 +446,7 @@ describe('templateSrv', function() { }); it('should replace $__interval_ms with interval milliseconds', function() { - var target = _templateSrv.replace('10 * $__interval_ms', { + const target = _templateSrv.replace('10 * $__interval_ms', { __interval_ms: { text: '100', value: '100' }, }); expect(target).toBe('10 * 100'); diff --git a/public/app/features/templating/specs/variable.test.ts b/public/app/features/templating/specs/variable.test.ts index cfe084957ec..814c5fbe003 100644 --- a/public/app/features/templating/specs/variable.test.ts +++ b/public/app/features/templating/specs/variable.test.ts @@ -2,38 +2,38 @@ import { containsVariable, assignModelProperties } from '../variable'; describe('containsVariable', function() { describe('when checking if a string contains a variable', function() { - it('should find it with $var syntax', function() { - var contains = containsVariable('this.$test.filters', 'test'); + it('should find it with $const syntax', function() { + const contains = containsVariable('this.$test.filters', 'test'); expect(contains).toBe(true); }); - it('should not find it if only part matches with $var syntax', function() { - var contains = containsVariable('this.$serverDomain.filters', 'server'); + it('should not find it if only part matches with $const syntax', function() { + const contains = containsVariable('this.$serverDomain.filters', 'server'); expect(contains).toBe(false); }); it('should find it if it ends with variable and passing multiple test strings', function() { - var contains = containsVariable('show field keys from $pgmetric', 'test string2', 'pgmetric'); + const contains = containsVariable('show field keys from $pgmetric', 'test string2', 'pgmetric'); expect(contains).toBe(true); }); it('should find it with [[var]] syntax', function() { - var contains = containsVariable('this.[[test]].filters', 'test'); + const contains = containsVariable('this.[[test]].filters', 'test'); expect(contains).toBe(true); }); it('should find it when part of segment', function() { - var contains = containsVariable('metrics.$env.$group-*', 'group'); + const contains = containsVariable('metrics.$env.$group-*', 'group'); expect(contains).toBe(true); }); it('should find it its the only thing', function() { - var contains = containsVariable('$env', 'env'); + const contains = containsVariable('$env', 'env'); expect(contains).toBe(true); }); it('should be able to pass in multiple test strings', function() { - var contains = containsVariable('asd', 'asd2.$env', 'env'); + const contains = containsVariable('asd', 'asd2.$env', 'env'); expect(contains).toBe(true); }); }); @@ -41,14 +41,14 @@ describe('containsVariable', function() { describe('assignModelProperties', function() { it('only set properties defined in defaults', function() { - var target: any = { test: 'asd' }; + const target: any = { test: 'asd' }; assignModelProperties(target, { propA: 1, propB: 2 }, { propB: 0 }); expect(target.propB).toBe(2); expect(target.test).toBe('asd'); }); it('use default value if not found on source', function() { - var target: any = { test: 'asd' }; + const target: any = { test: 'asd' }; assignModelProperties(target, { propA: 1, propB: 2 }, { propC: 10 }); expect(target.propC).toBe(10); }); diff --git a/public/app/features/templating/specs/variable_srv.test.ts b/public/app/features/templating/specs/variable_srv.test.ts index f7796434b5e..28fd3860ed3 100644 --- a/public/app/features/templating/specs/variable_srv.test.ts +++ b/public/app/features/templating/specs/variable_srv.test.ts @@ -4,7 +4,7 @@ import moment from 'moment'; import $q from 'q'; describe('VariableSrv', function() { - var ctx = { + const ctx = { datasourceSrv: {}, timeSrv: { timeRange: () => {}, @@ -33,7 +33,7 @@ describe('VariableSrv', function() { function describeUpdateVariable(desc, fn) { describe(desc, () => { - var scenario: any = {}; + const scenario: any = {}; scenario.setup = function(setupFn) { scenario.setupFn = setupFn; }; @@ -41,7 +41,7 @@ describe('VariableSrv', function() { beforeEach(async () => { scenario.setupFn(); - var ds: any = {}; + const ds: any = {}; ds.metricFindQuery = () => Promise.resolve(scenario.queryResult); ctx.variableSrv = new VariableSrv(ctx.$rootScope, $q, ctx.$location, ctx.$injector, ctx.templateSrv); @@ -100,7 +100,7 @@ describe('VariableSrv', function() { auto_count: 10, }; - var range = { + const range = { from: moment(new Date()) .subtract(7, 'days') .toDate(), @@ -118,7 +118,7 @@ describe('VariableSrv', function() { }); it('should set $__auto_interval_test', () => { - var call = ctx.templateSrv.setGrafanaVariable.mock.calls[0]; + const call = ctx.templateSrv.setGrafanaVariable.mock.calls[0]; expect(call[0]).toBe('$__auto_interval_test'); expect(call[1]).toBe('12h'); }); @@ -126,7 +126,7 @@ describe('VariableSrv', function() { // updateAutoValue() gets called twice: once directly once via VariableSrv.validateVariableSelectionState() // So use lastCall instead of a specific call number it('should set $__auto_interval', () => { - var call = ctx.templateSrv.setGrafanaVariable.mock.calls.pop(); + const call = ctx.templateSrv.setGrafanaVariable.mock.calls.pop(); expect(call[0]).toBe('$__auto_interval'); expect(call[1]).toBe('12h'); }); @@ -503,10 +503,10 @@ describe('VariableSrv', function() { }); describe('multiple interval variables with auto', () => { - var variable1, variable2; + let variable1, variable2; beforeEach(() => { - var range = { + const range = { from: moment(new Date()) .subtract(7, 'days') .toDate(), @@ -515,7 +515,7 @@ describe('VariableSrv', function() { ctx.timeSrv.timeRange = () => range; ctx.templateSrv.setGrafanaVariable = jest.fn(); - var variableModel1 = { + const variableModel1 = { type: 'interval', query: '1s,2h,5h,1d', name: 'variable1', @@ -525,7 +525,7 @@ describe('VariableSrv', function() { variable1 = ctx.variableSrv.createVariableFromModel(variableModel1); ctx.variableSrv.addVariable(variable1); - var variableModel2 = { + const variableModel2 = { type: 'interval', query: '1s,2h,5h', name: 'variable2', @@ -550,14 +550,14 @@ describe('VariableSrv', function() { }); it('should correctly set $__auto_interval_variableX', () => { - var variable1Set, + let variable1Set, variable2Set, legacySet, unknownSet = false; // updateAutoValue() gets called repeatedly: once directly once via VariableSrv.validateVariableSelectionState() // So check that all calls are valid rather than expect a specific number and/or ordering of calls - for (var i = 0; i < ctx.templateSrv.setGrafanaVariable.mock.calls.length; i++) { - var call = ctx.templateSrv.setGrafanaVariable.mock.calls[i]; + for (let i = 0; i < ctx.templateSrv.setGrafanaVariable.mock.calls.length; i++) { + const call = ctx.templateSrv.setGrafanaVariable.mock.calls[i]; switch (call[0]) { case '$__auto_interval_variable1': expect(call[1]).toBe('12h'); diff --git a/public/app/features/templating/specs/variable_srv_init.test.ts b/public/app/features/templating/specs/variable_srv_init.test.ts index e011d4d0d15..f06f533e429 100644 --- a/public/app/features/templating/specs/variable_srv_init.test.ts +++ b/public/app/features/templating/specs/variable_srv_init.test.ts @@ -26,7 +26,7 @@ describe('VariableSrv init', function() { function describeInitScenario(desc, fn) { describe(desc, () => { - var scenario: any = { + const scenario: any = { urlParams: {}, setup: setupFn => { scenario.setupFn = setupFn; @@ -92,7 +92,7 @@ describe('VariableSrv init', function() { }); describe('given dependent variables', () => { - var variableList = [ + const variableList = [ { name: 'app', type: 'query', @@ -110,7 +110,7 @@ describe('VariableSrv init', function() { }, ]; - describeInitScenario('when setting parent var from url', scenario => { + describeInitScenario('when setting parent const from url', scenario => { scenario.setup(() => { scenario.variables = _.cloneDeep(variableList); scenario.urlParams['var-app'] = 'google'; @@ -148,7 +148,7 @@ describe('VariableSrv init', function() { }); it('should update current value', () => { - var variable = ctx.variableSrv.variables[0]; + const variable = ctx.variableSrv.variables[0]; expect(variable.options.length).toBe(2); }); }); @@ -172,7 +172,7 @@ describe('VariableSrv init', function() { }); it('should update current value', () => { - var variable = ctx.variableSrv.variables[0]; + const variable = ctx.variableSrv.variables[0]; expect(variable.current.value.length).toBe(2); expect(variable.current.value[0]).toBe('val2'); expect(variable.current.value[1]).toBe('val1'); @@ -182,7 +182,7 @@ describe('VariableSrv init', function() { }); it('should set options that are not in value to selected false', () => { - var variable = ctx.variableSrv.variables[0]; + const variable = ctx.variableSrv.variables[0]; expect(variable.options[2].selected).toBe(false); }); }); @@ -206,7 +206,7 @@ describe('VariableSrv init', function() { }); it('should update current value', () => { - var variable = ctx.variableSrv.variables[0]; + const variable = ctx.variableSrv.variables[0]; expect(variable.current.value.length).toBe(2); expect(variable.current.value[0]).toBe('val2'); expect(variable.current.value[1]).toBe('val1'); @@ -216,7 +216,7 @@ describe('VariableSrv init', function() { }); it('should set options that are not in value to selected false', () => { - var variable = ctx.variableSrv.variables[0]; + const variable = ctx.variableSrv.variables[0]; expect(variable.options[2].selected).toBe(false); }); }); diff --git a/public/app/plugins/datasource/cloudwatch/specs/datasource.test.ts b/public/app/plugins/datasource/cloudwatch/specs/datasource.test.ts index eae3e91d37d..08329ba4e73 100644 --- a/public/app/plugins/datasource/cloudwatch/specs/datasource.test.ts +++ b/public/app/plugins/datasource/cloudwatch/specs/datasource.test.ts @@ -35,9 +35,9 @@ describe('CloudWatchDatasource', function() { }); describe('When performing CloudWatch query', function() { - var requestParams; + let requestParams; - var query = { + const query = { range: { from: 'now-1h', to: 'now' }, rangeRaw: { from: 1483228800, to: 1483232400 }, targets: [ @@ -54,7 +54,7 @@ describe('CloudWatchDatasource', function() { ], }; - var response = { + const response = { timings: [null], results: { A: { @@ -82,7 +82,7 @@ describe('CloudWatchDatasource', function() { it('should generate the correct query', function(done) { ctx.ds.query(query).then(function() { - var params = requestParams.queries[0]; + const params = requestParams.queries[0]; expect(params.namespace).toBe(query.targets[0].namespace); expect(params.metricName).toBe(query.targets[0].metricName); expect(params.dimensions['InstanceId']).toBe('i-12345678'); @@ -97,7 +97,7 @@ describe('CloudWatchDatasource', function() { period: '10m', }; - var query = { + const query = { range: { from: 'now-1h', to: 'now' }, rangeRaw: { from: 1483228800, to: 1483232400 }, targets: [ @@ -115,14 +115,14 @@ describe('CloudWatchDatasource', function() { }; ctx.ds.query(query).then(function() { - var params = requestParams.queries[0]; + const params = requestParams.queries[0]; expect(params.period).toBe('600'); done(); }); }); it('should cancel query for invalid extended statistics', function() { - var query = { + const query = { range: { from: 'now-1h', to: 'now' }, rangeRaw: { from: 1483228800, to: 1483232400 }, targets: [ @@ -152,7 +152,7 @@ describe('CloudWatchDatasource', function() { describe('When query region is "default"', function() { it('should return the datasource region if empty or "default"', function() { - var defaultRegion = instanceSettings.jsonData.defaultRegion; + const defaultRegion = instanceSettings.jsonData.defaultRegion; expect(ctx.ds.getActualRegion()).toBe(defaultRegion); expect(ctx.ds.getActualRegion('')).toBe(defaultRegion); @@ -163,7 +163,7 @@ describe('CloudWatchDatasource', function() { expect(ctx.ds.getActualRegion('some-fake-region-1')).toBe('some-fake-region-1'); }); - var requestParams; + let requestParams; beforeEach(function() { ctx.ds.performTimeSeriesQuery = jest.fn(request => { requestParams = request; @@ -172,7 +172,7 @@ describe('CloudWatchDatasource', function() { }); it('should query for the datasource region if empty or "default"', function(done) { - var query = { + const query = { range: { from: 'now-1h', to: 'now' }, rangeRaw: { from: 1483228800, to: 1483232400 }, targets: [ @@ -197,7 +197,7 @@ describe('CloudWatchDatasource', function() { }); describe('When performing CloudWatch query for extended statistics', function() { - var query = { + const query = { range: { from: 'now-1h', to: 'now' }, rangeRaw: { from: 1483228800, to: 1483232400 }, targets: [ @@ -215,7 +215,7 @@ describe('CloudWatchDatasource', function() { ], }; - var response = { + const response = { timings: [null], results: { A: { @@ -379,10 +379,10 @@ describe('CloudWatchDatasource', function() { }); it('should caclculate the correct period', function() { - var hourSec = 60 * 60; - var daySec = hourSec * 24; - var start = 1483196400 * 1000; - var testData: any[] = [ + const hourSec = 60 * 60; + const daySec = hourSec * 24; + const start = 1483196400 * 1000; + const testData: any[] = [ [ { period: 60, namespace: 'AWS/EC2' }, { range: { from: new Date(start), to: new Date(start + 3600 * 1000) } }, diff --git a/public/app/plugins/datasource/elasticsearch/specs/datasource.test.ts b/public/app/plugins/datasource/elasticsearch/specs/datasource.test.ts index d1e2e3ba835..d37d1d86d54 100644 --- a/public/app/plugins/datasource/elasticsearch/specs/datasource.test.ts +++ b/public/app/plugins/datasource/elasticsearch/specs/datasource.test.ts @@ -53,7 +53,7 @@ describe('ElasticDatasource', function() { }); it('should translate index pattern to current day', function() { - var requestOptions; + let requestOptions; ctx.backendSrv.datasourceRequest = jest.fn(options => { requestOptions = options; return Promise.resolve({ data: {} }); @@ -61,13 +61,13 @@ describe('ElasticDatasource', function() { ctx.ds.testDatasource(); - var today = moment.utc().format('YYYY.MM.DD'); + const today = moment.utc().format('YYYY.MM.DD'); expect(requestOptions.url).toBe('http://es.com/asd-' + today + '/_mapping'); }); }); describe('When issuing metric query with interval pattern', function() { - var requestOptions, parts, header; + let requestOptions, parts, header; beforeEach(() => { createDatasource({ @@ -104,13 +104,13 @@ describe('ElasticDatasource', function() { }); it('should json escape lucene query', function() { - var body = angular.fromJson(parts[1]); + const body = angular.fromJson(parts[1]); expect(body.query.bool.filter[1].query_string.query).toBe('escape\\:test'); }); }); describe('When issuing document query', function() { - var requestOptions, parts, header; + let requestOptions, parts, header; beforeEach(function() { createDatasource({ @@ -147,7 +147,7 @@ describe('ElasticDatasource', function() { }); it('should set size', function() { - var body = angular.fromJson(parts[1]); + const body = angular.fromJson(parts[1]); expect(body.size).toBe(500); }); }); @@ -210,7 +210,7 @@ describe('ElasticDatasource', function() { query: '*', }) .then(fieldObjects => { - var fields = _.map(fieldObjects, 'text'); + const fields = _.map(fieldObjects, 'text'); expect(fields).toEqual([ '@timestamp', 'beat.name.raw', @@ -232,7 +232,7 @@ describe('ElasticDatasource', function() { type: 'number', }) .then(fieldObjects => { - var fields = _.map(fieldObjects, 'text'); + const fields = _.map(fieldObjects, 'text'); expect(fields).toEqual(['system.cpu.system', 'system.cpu.user', 'system.process.cpu.total']); }); @@ -243,14 +243,14 @@ describe('ElasticDatasource', function() { type: 'date', }) .then(fieldObjects => { - var fields = _.map(fieldObjects, 'text'); + const fields = _.map(fieldObjects, 'text'); expect(fields).toEqual(['@timestamp']); }); }); }); describe('When issuing aggregation query on es5.x', function() { - var requestOptions, parts, header; + let requestOptions, parts, header; beforeEach(function() { createDatasource({ @@ -287,13 +287,13 @@ describe('ElasticDatasource', function() { }); it('should set size to 0', function() { - var body = angular.fromJson(parts[1]); + const body = angular.fromJson(parts[1]); expect(body.size).toBe(0); }); }); describe('When issuing metricFind query on es5.x', function() { - var requestOptions, parts, header, body, results; + let requestOptions, parts, header, body, results; beforeEach(() => { createDatasource({ diff --git a/public/app/plugins/datasource/elasticsearch/specs/index_pattern.test.ts b/public/app/plugins/datasource/elasticsearch/specs/index_pattern.test.ts index 2f921e10425..c5cf4c9dee0 100644 --- a/public/app/plugins/datasource/elasticsearch/specs/index_pattern.test.ts +++ b/public/app/plugins/datasource/elasticsearch/specs/index_pattern.test.ts @@ -6,8 +6,8 @@ import { IndexPattern } from '../index_pattern'; describe('IndexPattern', () => { describe('when getting index for today', () => { test('should return correct index name', () => { - var pattern = new IndexPattern('[asd-]YYYY.MM.DD', 'Daily'); - var expected = 'asd-' + moment.utc().format('YYYY.MM.DD'); + const pattern = new IndexPattern('[asd-]YYYY.MM.DD', 'Daily'); + const expected = 'asd-' + moment.utc().format('YYYY.MM.DD'); expect(pattern.getIndexForToday()).toBe(expected); }); @@ -16,20 +16,20 @@ describe('IndexPattern', () => { describe('when getting index list for time range', () => { describe('no interval', () => { test('should return correct index', () => { - var pattern = new IndexPattern('my-metrics', null); - var from = new Date(2015, 4, 30, 1, 2, 3); - var to = new Date(2015, 5, 1, 12, 5, 6); + const pattern = new IndexPattern('my-metrics', null); + const from = new Date(2015, 4, 30, 1, 2, 3); + const to = new Date(2015, 5, 1, 12, 5, 6); expect(pattern.getIndexList(from, to)).toEqual('my-metrics'); }); }); describe('daily', () => { test('should return correct index list', () => { - var pattern = new IndexPattern('[asd-]YYYY.MM.DD', 'Daily'); - var from = new Date(1432940523000); - var to = new Date(1433153106000); + const pattern = new IndexPattern('[asd-]YYYY.MM.DD', 'Daily'); + const from = new Date(1432940523000); + const to = new Date(1433153106000); - var expected = ['asd-2015.05.29', 'asd-2015.05.30', 'asd-2015.05.31', 'asd-2015.06.01']; + const expected = ['asd-2015.05.29', 'asd-2015.05.30', 'asd-2015.05.31', 'asd-2015.06.01']; expect(pattern.getIndexList(from, to)).toEqual(expected); }); diff --git a/public/app/plugins/datasource/elasticsearch/specs/query_builder.test.ts b/public/app/plugins/datasource/elasticsearch/specs/query_builder.test.ts index 1dde47915d9..e4c9404e667 100644 --- a/public/app/plugins/datasource/elasticsearch/specs/query_builder.test.ts +++ b/public/app/plugins/datasource/elasticsearch/specs/query_builder.test.ts @@ -1,14 +1,14 @@ import { ElasticQueryBuilder } from '../query_builder'; describe('ElasticQueryBuilder', () => { - var builder; + let builder; beforeEach(() => { builder = new ElasticQueryBuilder({ timeField: '@timestamp' }); }); it('with defaults', () => { - var query = builder.build({ + const query = builder.build({ metrics: [{ type: 'Count', id: '0' }], timeField: '@timestamp', bucketAggs: [{ type: 'date_histogram', field: '@timestamp', id: '1' }], @@ -19,12 +19,12 @@ describe('ElasticQueryBuilder', () => { }); it('with defaults on es5.x', () => { - var builder_5x = new ElasticQueryBuilder({ + const builder_5x = new ElasticQueryBuilder({ timeField: '@timestamp', esVersion: 5, }); - var query = builder_5x.build({ + const query = builder_5x.build({ metrics: [{ type: 'Count', id: '0' }], timeField: '@timestamp', bucketAggs: [{ type: 'date_histogram', field: '@timestamp', id: '1' }], @@ -35,7 +35,7 @@ describe('ElasticQueryBuilder', () => { }); it('with multiple bucket aggs', () => { - var query = builder.build({ + const query = builder.build({ metrics: [{ type: 'count', id: '1' }], timeField: '@timestamp', bucketAggs: [ @@ -49,7 +49,7 @@ describe('ElasticQueryBuilder', () => { }); it('with select field', () => { - var query = builder.build( + const query = builder.build( { metrics: [{ type: 'avg', field: '@value', id: '1' }], bucketAggs: [{ type: 'date_histogram', field: '@timestamp', id: '2' }], @@ -58,12 +58,12 @@ describe('ElasticQueryBuilder', () => { 1000 ); - var aggs = query.aggs['2'].aggs; + const aggs = query.aggs['2'].aggs; expect(aggs['1'].avg.field).toBe('@value'); }); it('with term agg and order by metric agg', () => { - var query = builder.build( + const query = builder.build( { metrics: [{ type: 'count', id: '1' }, { type: 'avg', field: '@value', id: '5' }], bucketAggs: [ @@ -80,15 +80,15 @@ describe('ElasticQueryBuilder', () => { 1000 ); - var firstLevel = query.aggs['2']; - var secondLevel = firstLevel.aggs['3']; + const firstLevel = query.aggs['2']; + const secondLevel = firstLevel.aggs['3']; expect(firstLevel.aggs['5'].avg.field).toBe('@value'); expect(secondLevel.aggs['5'].avg.field).toBe('@value'); }); it('with metric percentiles', () => { - var query = builder.build( + const query = builder.build( { metrics: [ { @@ -106,14 +106,14 @@ describe('ElasticQueryBuilder', () => { 1000 ); - var firstLevel = query.aggs['3']; + const firstLevel = query.aggs['3']; expect(firstLevel.aggs['1'].percentiles.field).toBe('@load_time'); expect(firstLevel.aggs['1'].percentiles.percents).toEqual([1, 2, 3, 4]); }); it('with filters aggs', () => { - var query = builder.build({ + const query = builder.build({ metrics: [{ type: 'count', id: '1' }], timeField: '@timestamp', bucketAggs: [ @@ -134,11 +134,11 @@ describe('ElasticQueryBuilder', () => { }); it('with filters aggs on es5.x', () => { - var builder_5x = new ElasticQueryBuilder({ + const builder_5x = new ElasticQueryBuilder({ timeField: '@timestamp', esVersion: 5, }); - var query = builder_5x.build({ + const query = builder_5x.build({ metrics: [{ type: 'count', id: '1' }], timeField: '@timestamp', bucketAggs: [ @@ -159,7 +159,7 @@ describe('ElasticQueryBuilder', () => { }); it('with raw_document metric', () => { - var query = builder.build({ + const query = builder.build({ metrics: [{ type: 'raw_document', id: '1', settings: {} }], timeField: '@timestamp', bucketAggs: [], @@ -168,7 +168,7 @@ describe('ElasticQueryBuilder', () => { expect(query.size).toBe(500); }); it('with raw_document metric size set', () => { - var query = builder.build({ + const query = builder.build({ metrics: [{ type: 'raw_document', id: '1', settings: { size: 1337 } }], timeField: '@timestamp', bucketAggs: [], @@ -178,7 +178,7 @@ describe('ElasticQueryBuilder', () => { }); it('with moving average', () => { - var query = builder.build({ + const query = builder.build({ metrics: [ { id: '3', @@ -195,7 +195,7 @@ describe('ElasticQueryBuilder', () => { bucketAggs: [{ type: 'date_histogram', field: '@timestamp', id: '3' }], }); - var firstLevel = query.aggs['3']; + const firstLevel = query.aggs['3']; expect(firstLevel.aggs['2']).not.toBe(undefined); expect(firstLevel.aggs['2'].moving_avg).not.toBe(undefined); @@ -203,7 +203,7 @@ describe('ElasticQueryBuilder', () => { }); it('with broken moving average', () => { - var query = builder.build({ + const query = builder.build({ metrics: [ { id: '3', @@ -224,7 +224,7 @@ describe('ElasticQueryBuilder', () => { bucketAggs: [{ type: 'date_histogram', field: '@timestamp', id: '3' }], }); - var firstLevel = query.aggs['3']; + const firstLevel = query.aggs['3']; expect(firstLevel.aggs['2']).not.toBe(undefined); expect(firstLevel.aggs['2'].moving_avg).not.toBe(undefined); @@ -233,7 +233,7 @@ describe('ElasticQueryBuilder', () => { }); it('with derivative', () => { - var query = builder.build({ + const query = builder.build({ metrics: [ { id: '3', @@ -249,7 +249,7 @@ describe('ElasticQueryBuilder', () => { bucketAggs: [{ type: 'date_histogram', field: '@timestamp', id: '3' }], }); - var firstLevel = query.aggs['3']; + const firstLevel = query.aggs['3']; expect(firstLevel.aggs['2']).not.toBe(undefined); expect(firstLevel.aggs['2'].derivative).not.toBe(undefined); @@ -257,7 +257,7 @@ describe('ElasticQueryBuilder', () => { }); it('with histogram', () => { - var query = builder.build({ + const query = builder.build({ metrics: [{ id: '1', type: 'count' }], bucketAggs: [ { @@ -269,7 +269,7 @@ describe('ElasticQueryBuilder', () => { ], }); - var firstLevel = query.aggs['3']; + const firstLevel = query.aggs['3']; expect(firstLevel.histogram.field).toBe('bytes'); expect(firstLevel.histogram.interval).toBe(10); expect(firstLevel.histogram.min_doc_count).toBe(2); @@ -277,7 +277,7 @@ describe('ElasticQueryBuilder', () => { }); it('with adhoc filters', () => { - var query = builder.build( + const query = builder.build( { metrics: [{ type: 'Count', id: '0' }], timeField: '@timestamp', diff --git a/public/app/plugins/datasource/elasticsearch/specs/query_def.test.ts b/public/app/plugins/datasource/elasticsearch/specs/query_def.test.ts index 0102e5febfb..471d400037c 100644 --- a/public/app/plugins/datasource/elasticsearch/specs/query_def.test.ts +++ b/public/app/plugins/datasource/elasticsearch/specs/query_def.test.ts @@ -3,7 +3,7 @@ import * as queryDef from '../query_def'; describe('ElasticQueryDef', () => { describe('getPipelineAggOptions', () => { describe('with zero targets', () => { - var response = queryDef.getPipelineAggOptions([]); + const response = queryDef.getPipelineAggOptions([]); test('should return zero', () => { expect(response.length).toBe(0); @@ -11,11 +11,11 @@ describe('ElasticQueryDef', () => { }); describe('with count and sum targets', () => { - var targets = { + const targets = { metrics: [{ type: 'count', field: '@value' }, { type: 'sum', field: '@value' }], }; - var response = queryDef.getPipelineAggOptions(targets); + const response = queryDef.getPipelineAggOptions(targets); test('should return zero', () => { expect(response.length).toBe(2); @@ -23,11 +23,11 @@ describe('ElasticQueryDef', () => { }); describe('with count and moving average targets', () => { - var targets = { + const targets = { metrics: [{ type: 'count', field: '@value' }, { type: 'moving_avg', field: '@value' }], }; - var response = queryDef.getPipelineAggOptions(targets); + const response = queryDef.getPipelineAggOptions(targets); test('should return one', () => { expect(response.length).toBe(1); @@ -35,11 +35,11 @@ describe('ElasticQueryDef', () => { }); describe('with derivatives targets', () => { - var targets = { + const targets = { metrics: [{ type: 'derivative', field: '@value' }], }; - var response = queryDef.getPipelineAggOptions(targets); + const response = queryDef.getPipelineAggOptions(targets); test('should return zero', () => { expect(response.length).toBe(0); @@ -49,7 +49,7 @@ describe('ElasticQueryDef', () => { describe('isPipelineMetric', () => { describe('moving_avg', () => { - var result = queryDef.isPipelineAgg('moving_avg'); + const result = queryDef.isPipelineAgg('moving_avg'); test('is pipe line metric', () => { expect(result).toBe(true); @@ -57,7 +57,7 @@ describe('ElasticQueryDef', () => { }); describe('count', () => { - var result = queryDef.isPipelineAgg('count'); + const result = queryDef.isPipelineAgg('count'); test('is not pipe line metric', () => { expect(result).toBe(false); diff --git a/public/app/plugins/datasource/grafana-live/datasource.ts b/public/app/plugins/datasource/grafana-live/datasource.ts index 5cba43dd2f9..d861400b2c8 100644 --- a/public/app/plugins/datasource/grafana-live/datasource.ts +++ b/public/app/plugins/datasource/grafana-live/datasource.ts @@ -8,7 +8,7 @@ class DataObservable { } subscribe(options) { - var observable = liveSrv.subscribe(this.target.stream); + const observable = liveSrv.subscribe(this.target.stream); return observable.subscribe(data => { console.log('grafana stream ds data!', data); }); @@ -26,8 +26,8 @@ export class GrafanaStreamDS { return Promise.resolve({ data: [] }); } - var target = options.targets[0]; - var observable = new DataObservable(target); + const target = options.targets[0]; + const observable = new DataObservable(target); return Promise.resolve(observable); } diff --git a/public/app/plugins/datasource/graphite/specs/datasource.test.ts b/public/app/plugins/datasource/graphite/specs/datasource.test.ts index 826f2fed344..563f1047cdb 100644 --- a/public/app/plugins/datasource/graphite/specs/datasource.test.ts +++ b/public/app/plugins/datasource/graphite/specs/datasource.test.ts @@ -324,7 +324,7 @@ function accessScenario(name, url, fn) { it('tracing headers should be added', () => { ctx.instanceSettings.url = url; - var ds = new GraphiteDatasource(ctx.instanceSettings, ctx.$q, ctx.backendSrv, ctx.templateSrv); + const ds = new GraphiteDatasource(ctx.instanceSettings, ctx.$q, ctx.backendSrv, ctx.templateSrv); ds.addTracingHeaders(httpOptions, options); fn(httpOptions); }); diff --git a/public/app/plugins/datasource/graphite/specs/gfunc.test.ts b/public/app/plugins/datasource/graphite/specs/gfunc.test.ts index 08373582e73..61a0e896b0f 100644 --- a/public/app/plugins/datasource/graphite/specs/gfunc.test.ts +++ b/public/app/plugins/datasource/graphite/specs/gfunc.test.ts @@ -2,7 +2,7 @@ import gfunc from '../gfunc'; describe('when creating func instance from func names', function() { it('should return func instance', function() { - var func = gfunc.createFuncInstance('sumSeries'); + const func = gfunc.createFuncInstance('sumSeries'); expect(func).toBeTruthy(); expect(func.def.name).toEqual('sumSeries'); expect(func.def.params.length).toEqual(1); @@ -11,18 +11,18 @@ describe('when creating func instance from func names', function() { }); it('should return func instance with shortName', function() { - var func = gfunc.createFuncInstance('sum'); + const func = gfunc.createFuncInstance('sum'); expect(func).toBeTruthy(); }); it('should return func instance from funcDef', function() { - var func = gfunc.createFuncInstance('sum'); - var func2 = gfunc.createFuncInstance(func.def); + const func = gfunc.createFuncInstance('sum'); + const func2 = gfunc.createFuncInstance(func.def); expect(func2).toBeTruthy(); }); it('func instance should have text representation', function() { - var func = gfunc.createFuncInstance('groupByNode'); + const func = gfunc.createFuncInstance('groupByNode'); func.params[0] = 5; func.params[1] = 'avg'; func.updateText(); @@ -32,62 +32,62 @@ describe('when creating func instance from func names', function() { describe('when rendering func instance', function() { it('should handle single metric param', function() { - var func = gfunc.createFuncInstance('sumSeries'); + const func = gfunc.createFuncInstance('sumSeries'); expect(func.render('hello.metric')).toEqual('sumSeries(hello.metric)'); }); it('should include default params if options enable it', function() { - var func = gfunc.createFuncInstance('scaleToSeconds', { + const func = gfunc.createFuncInstance('scaleToSeconds', { withDefaultParams: true, }); expect(func.render('hello')).toEqual('scaleToSeconds(hello, 1)'); }); it('should handle int or interval params with number', function() { - var func = gfunc.createFuncInstance('movingMedian'); + const func = gfunc.createFuncInstance('movingMedian'); func.params[0] = '5'; expect(func.render('hello')).toEqual('movingMedian(hello, 5)'); }); it('should handle int or interval params with interval string', function() { - var func = gfunc.createFuncInstance('movingMedian'); + const func = gfunc.createFuncInstance('movingMedian'); func.params[0] = '5min'; expect(func.render('hello')).toEqual("movingMedian(hello, '5min')"); }); it('should never quote boolean paramater', function() { - var func = gfunc.createFuncInstance('sortByName'); + const func = gfunc.createFuncInstance('sortByName'); func.params[0] = '$natural'; expect(func.render('hello')).toEqual('sortByName(hello, $natural)'); }); it('should never quote int paramater', function() { - var func = gfunc.createFuncInstance('maximumAbove'); + const func = gfunc.createFuncInstance('maximumAbove'); func.params[0] = '$value'; expect(func.render('hello')).toEqual('maximumAbove(hello, $value)'); }); it('should never quote node paramater', function() { - var func = gfunc.createFuncInstance('aliasByNode'); + const func = gfunc.createFuncInstance('aliasByNode'); func.params[0] = '$node'; expect(func.render('hello')).toEqual('aliasByNode(hello, $node)'); }); it('should handle metric param and int param and string param', function() { - var func = gfunc.createFuncInstance('groupByNode'); + const func = gfunc.createFuncInstance('groupByNode'); func.params[0] = 5; func.params[1] = 'avg'; expect(func.render('hello.metric')).toEqual("groupByNode(hello.metric, 5, 'avg')"); }); it('should handle function with no metric param', function() { - var func = gfunc.createFuncInstance('randomWalk'); + const func = gfunc.createFuncInstance('randomWalk'); func.params[0] = 'test'; expect(func.render(undefined)).toEqual("randomWalk('test')"); }); it('should handle function multiple series params', function() { - var func = gfunc.createFuncInstance('asPercent'); + const func = gfunc.createFuncInstance('asPercent'); func.params[0] = '#B'; expect(func.render('#A')).toEqual('asPercent(#A, #B)'); }); @@ -95,14 +95,14 @@ describe('when rendering func instance', function() { describe('when requesting function definitions', function() { it('should return function definitions', function() { - var funcIndex = gfunc.getFuncDefs('1.0'); + const funcIndex = gfunc.getFuncDefs('1.0'); expect(Object.keys(funcIndex).length).toBeGreaterThan(8); }); }); describe('when updating func param', function() { it('should update param value and update text representation', function() { - var func = gfunc.createFuncInstance('summarize', { + const func = gfunc.createFuncInstance('summarize', { withDefaultParams: true, }); func.updateParam('1h', 0); @@ -111,7 +111,7 @@ describe('when updating func param', function() { }); it('should parse numbers as float', function() { - var func = gfunc.createFuncInstance('scale'); + const func = gfunc.createFuncInstance('scale'); func.updateParam('0.001', 0); expect(func.params[0]).toBe('0.001'); }); @@ -119,13 +119,13 @@ describe('when updating func param', function() { describe('when updating func param with optional second parameter', function() { it('should update value and text', function() { - var func = gfunc.createFuncInstance('aliasByNode'); + const func = gfunc.createFuncInstance('aliasByNode'); func.updateParam('1', 0); expect(func.params[0]).toBe('1'); }); it('should slit text and put value in second param', function() { - var func = gfunc.createFuncInstance('aliasByNode'); + const func = gfunc.createFuncInstance('aliasByNode'); func.updateParam('4,-5', 0); expect(func.params[0]).toBe('4'); expect(func.params[1]).toBe('-5'); @@ -133,7 +133,7 @@ describe('when updating func param with optional second parameter', function() { }); it('should remove second param when empty string is set', function() { - var func = gfunc.createFuncInstance('aliasByNode'); + const func = gfunc.createFuncInstance('aliasByNode'); func.updateParam('4,-5', 0); func.updateParam('', 1); expect(func.params[0]).toBe('4'); diff --git a/public/app/plugins/datasource/graphite/specs/lexer.test.ts b/public/app/plugins/datasource/graphite/specs/lexer.test.ts index c925e5cdaba..f00df17a725 100644 --- a/public/app/plugins/datasource/graphite/specs/lexer.test.ts +++ b/public/app/plugins/datasource/graphite/specs/lexer.test.ts @@ -2,8 +2,8 @@ import { Lexer } from '../lexer'; describe('when lexing graphite expression', function() { it('should tokenize metric expression', function() { - var lexer = new Lexer('metric.test.*.asd.count'); - var tokens = lexer.tokenize(); + const lexer = new Lexer('metric.test.*.asd.count'); + const tokens = lexer.tokenize(); expect(tokens[0].value).toBe('metric'); expect(tokens[1].value).toBe('.'); expect(tokens[2].type).toBe('identifier'); @@ -12,36 +12,36 @@ describe('when lexing graphite expression', function() { }); it('should tokenize metric expression with dash', function() { - var lexer = new Lexer('metric.test.se1-server-*.asd.count'); - var tokens = lexer.tokenize(); + const lexer = new Lexer('metric.test.se1-server-*.asd.count'); + const tokens = lexer.tokenize(); expect(tokens[4].type).toBe('identifier'); expect(tokens[4].value).toBe('se1-server-*'); }); it('should tokenize metric expression with dash2', function() { - var lexer = new Lexer('net.192-168-1-1.192-168-1-9.ping_value.*'); - var tokens = lexer.tokenize(); + const lexer = new Lexer('net.192-168-1-1.192-168-1-9.ping_value.*'); + const tokens = lexer.tokenize(); expect(tokens[0].value).toBe('net'); expect(tokens[2].value).toBe('192-168-1-1'); }); it('should tokenize metric expression with equal sign', function() { - var lexer = new Lexer('apps=test'); - var tokens = lexer.tokenize(); + const lexer = new Lexer('apps=test'); + const tokens = lexer.tokenize(); expect(tokens[0].value).toBe('apps=test'); }); it('simple function2', function() { - var lexer = new Lexer('offset(test.metric, -100)'); - var tokens = lexer.tokenize(); + const lexer = new Lexer('offset(test.metric, -100)'); + const tokens = lexer.tokenize(); expect(tokens[2].type).toBe('identifier'); expect(tokens[4].type).toBe('identifier'); expect(tokens[6].type).toBe('number'); }); it('should tokenize metric expression with curly braces', function() { - var lexer = new Lexer('metric.se1-{first, second}.count'); - var tokens = lexer.tokenize(); + const lexer = new Lexer('metric.se1-{first, second}.count'); + const tokens = lexer.tokenize(); expect(tokens.length).toBe(10); expect(tokens[3].type).toBe('{'); expect(tokens[4].value).toBe('first'); @@ -50,8 +50,8 @@ describe('when lexing graphite expression', function() { }); it('should tokenize metric expression with number segments', function() { - var lexer = new Lexer('metric.10.12_10.test'); - var tokens = lexer.tokenize(); + const lexer = new Lexer('metric.10.12_10.test'); + const tokens = lexer.tokenize(); expect(tokens[0].type).toBe('identifier'); expect(tokens[2].type).toBe('identifier'); expect(tokens[2].value).toBe('10'); @@ -60,16 +60,16 @@ describe('when lexing graphite expression', function() { }); it('should tokenize metric expression with segment that start with number', function() { - var lexer = new Lexer('metric.001-server'); - var tokens = lexer.tokenize(); + const lexer = new Lexer('metric.001-server'); + const tokens = lexer.tokenize(); expect(tokens[0].type).toBe('identifier'); expect(tokens[2].type).toBe('identifier'); expect(tokens.length).toBe(3); }); it('should tokenize func call with numbered metric and number arg', function() { - var lexer = new Lexer('scale(metric.10, 15)'); - var tokens = lexer.tokenize(); + const lexer = new Lexer('scale(metric.10, 15)'); + const tokens = lexer.tokenize(); expect(tokens[0].type).toBe('identifier'); expect(tokens[2].type).toBe('identifier'); expect(tokens[2].value).toBe('metric'); @@ -79,24 +79,24 @@ describe('when lexing graphite expression', function() { }); it('should tokenize metric with template parameter', function() { - var lexer = new Lexer('metric.[[server]].test'); - var tokens = lexer.tokenize(); + const lexer = new Lexer('metric.[[server]].test'); + const tokens = lexer.tokenize(); expect(tokens[2].type).toBe('identifier'); expect(tokens[2].value).toBe('[[server]]'); expect(tokens[4].type).toBe('identifier'); }); it('should tokenize metric with question mark', function() { - var lexer = new Lexer('metric.server_??.test'); - var tokens = lexer.tokenize(); + const lexer = new Lexer('metric.server_??.test'); + const tokens = lexer.tokenize(); expect(tokens[2].type).toBe('identifier'); expect(tokens[2].value).toBe('server_??'); expect(tokens[4].type).toBe('identifier'); }); it('should handle error with unterminated string', function() { - var lexer = new Lexer("alias(metric, 'asd)"); - var tokens = lexer.tokenize(); + const lexer = new Lexer("alias(metric, 'asd)"); + const tokens = lexer.tokenize(); expect(tokens[0].value).toBe('alias'); expect(tokens[1].value).toBe('('); expect(tokens[2].value).toBe('metric'); @@ -107,15 +107,15 @@ describe('when lexing graphite expression', function() { }); it('should handle float parameters', function() { - var lexer = new Lexer('alias(metric, 0.002)'); - var tokens = lexer.tokenize(); + const lexer = new Lexer('alias(metric, 0.002)'); + const tokens = lexer.tokenize(); expect(tokens[4].type).toBe('number'); expect(tokens[4].value).toBe('0.002'); }); it('should handle bool parameters', function() { - var lexer = new Lexer('alias(metric, true, false)'); - var tokens = lexer.tokenize(); + const lexer = new Lexer('alias(metric, true, false)'); + const tokens = lexer.tokenize(); expect(tokens[4].type).toBe('bool'); expect(tokens[4].value).toBe('true'); expect(tokens[6].type).toBe('bool'); diff --git a/public/app/plugins/datasource/graphite/specs/parser.test.ts b/public/app/plugins/datasource/graphite/specs/parser.test.ts index 7964d9d257b..966eb213d64 100644 --- a/public/app/plugins/datasource/graphite/specs/parser.test.ts +++ b/public/app/plugins/datasource/graphite/specs/parser.test.ts @@ -2,8 +2,8 @@ import { Parser } from '../parser'; describe('when parsing', function() { it('simple metric expression', function() { - var parser = new Parser('metric.test.*.asd.count'); - var rootNode = parser.getAst(); + const parser = new Parser('metric.test.*.asd.count'); + const rootNode = parser.getAst(); expect(rootNode.type).toBe('metric'); expect(rootNode.segments.length).toBe(5); @@ -11,8 +11,8 @@ describe('when parsing', function() { }); it('simple metric expression with numbers in segments', function() { - var parser = new Parser('metric.10.15_20.5'); - var rootNode = parser.getAst(); + const parser = new Parser('metric.10.15_20.5'); + const rootNode = parser.getAst(); expect(rootNode.type).toBe('metric'); expect(rootNode.segments.length).toBe(4); @@ -22,8 +22,8 @@ describe('when parsing', function() { }); it('simple metric expression with curly braces', function() { - var parser = new Parser('metric.se1-{count, max}'); - var rootNode = parser.getAst(); + const parser = new Parser('metric.se1-{count, max}'); + const rootNode = parser.getAst(); expect(rootNode.type).toBe('metric'); expect(rootNode.segments.length).toBe(2); @@ -31,8 +31,8 @@ describe('when parsing', function() { }); it('simple metric expression with curly braces at start of segment and with post chars', function() { - var parser = new Parser('metric.{count, max}-something.count'); - var rootNode = parser.getAst(); + const parser = new Parser('metric.{count, max}-something.count'); + const rootNode = parser.getAst(); expect(rootNode.type).toBe('metric'); expect(rootNode.segments.length).toBe(3); @@ -40,31 +40,31 @@ describe('when parsing', function() { }); it('simple function', function() { - var parser = new Parser('sum(test)'); - var rootNode = parser.getAst(); + const parser = new Parser('sum(test)'); + const rootNode = parser.getAst(); expect(rootNode.type).toBe('function'); expect(rootNode.params.length).toBe(1); }); it('simple function2', function() { - var parser = new Parser('offset(test.metric, -100)'); - var rootNode = parser.getAst(); + const parser = new Parser('offset(test.metric, -100)'); + const rootNode = parser.getAst(); expect(rootNode.type).toBe('function'); expect(rootNode.params[0].type).toBe('metric'); expect(rootNode.params[1].type).toBe('number'); }); it('simple function with string arg', function() { - var parser = new Parser("randomWalk('test')"); - var rootNode = parser.getAst(); + const parser = new Parser("randomWalk('test')"); + const rootNode = parser.getAst(); expect(rootNode.type).toBe('function'); expect(rootNode.params.length).toBe(1); expect(rootNode.params[0].type).toBe('string'); }); it('function with multiple args', function() { - var parser = new Parser("sum(test, 1, 'test')"); - var rootNode = parser.getAst(); + const parser = new Parser("sum(test, 1, 'test')"); + const rootNode = parser.getAst(); expect(rootNode.type).toBe('function'); expect(rootNode.params.length).toBe(3); @@ -74,8 +74,8 @@ describe('when parsing', function() { }); it('function with nested function', function() { - var parser = new Parser('sum(scaleToSeconds(test, 1))'); - var rootNode = parser.getAst(); + const parser = new Parser('sum(scaleToSeconds(test, 1))'); + const rootNode = parser.getAst(); expect(rootNode.type).toBe('function'); expect(rootNode.params.length).toBe(1); @@ -87,8 +87,8 @@ describe('when parsing', function() { }); it('function with multiple series', function() { - var parser = new Parser('sum(test.test.*.count, test.timers.*.count)'); - var rootNode = parser.getAst(); + const parser = new Parser('sum(test.test.*.count, test.timers.*.count)'); + const rootNode = parser.getAst(); expect(rootNode.type).toBe('function'); expect(rootNode.params.length).toBe(2); @@ -97,8 +97,8 @@ describe('when parsing', function() { }); it('function with templated series', function() { - var parser = new Parser('sum(test.[[server]].count)'); - var rootNode = parser.getAst(); + const parser = new Parser('sum(test.[[server]].count)'); + const rootNode = parser.getAst(); expect(rootNode.message).toBe(undefined); expect(rootNode.params[0].type).toBe('metric'); @@ -107,54 +107,54 @@ describe('when parsing', function() { }); it('invalid metric expression', function() { - var parser = new Parser('metric.test.*.asd.'); - var rootNode = parser.getAst(); + const parser = new Parser('metric.test.*.asd.'); + const rootNode = parser.getAst(); expect(rootNode.message).toBe('Expected metric identifier instead found end of string'); expect(rootNode.pos).toBe(19); }); it('invalid function expression missing closing parenthesis', function() { - var parser = new Parser('sum(test'); - var rootNode = parser.getAst(); + const parser = new Parser('sum(test'); + const rootNode = parser.getAst(); expect(rootNode.message).toBe('Expected closing parenthesis instead found end of string'); expect(rootNode.pos).toBe(9); }); it('unclosed string in function', function() { - var parser = new Parser("sum('test)"); - var rootNode = parser.getAst(); + const parser = new Parser("sum('test)"); + const rootNode = parser.getAst(); expect(rootNode.message).toBe('Unclosed string parameter'); expect(rootNode.pos).toBe(11); }); it('handle issue #69', function() { - var parser = new Parser('cactiStyle(offset(scale(net.192-168-1-1.192-168-1-9.ping_value.*,0.001),-100))'); - var rootNode = parser.getAst(); + const parser = new Parser('cactiStyle(offset(scale(net.192-168-1-1.192-168-1-9.ping_value.*,0.001),-100))'); + const rootNode = parser.getAst(); expect(rootNode.type).toBe('function'); }); it('handle float function arguments', function() { - var parser = new Parser('scale(test, 0.002)'); - var rootNode = parser.getAst(); + const parser = new Parser('scale(test, 0.002)'); + const rootNode = parser.getAst(); expect(rootNode.type).toBe('function'); expect(rootNode.params[1].type).toBe('number'); expect(rootNode.params[1].value).toBe(0.002); }); it('handle curly brace pattern at start', function() { - var parser = new Parser('{apps}.test'); - var rootNode = parser.getAst(); + const parser = new Parser('{apps}.test'); + const rootNode = parser.getAst(); expect(rootNode.type).toBe('metric'); expect(rootNode.segments[0].value).toBe('{apps}'); expect(rootNode.segments[1].value).toBe('test'); }); it('series parameters', function() { - var parser = new Parser('asPercent(#A, #B)'); - var rootNode = parser.getAst(); + const parser = new Parser('asPercent(#A, #B)'); + const rootNode = parser.getAst(); expect(rootNode.type).toBe('function'); expect(rootNode.params[0].type).toBe('series-ref'); expect(rootNode.params[0].value).toBe('#A'); @@ -162,8 +162,8 @@ describe('when parsing', function() { }); it('series parameters, issue 2788', function() { - var parser = new Parser("summarize(diffSeries(#A, #B), '10m', 'sum', false)"); - var rootNode = parser.getAst(); + const parser = new Parser("summarize(diffSeries(#A, #B), '10m', 'sum', false)"); + const rootNode = parser.getAst(); expect(rootNode.type).toBe('function'); expect(rootNode.params[0].type).toBe('function'); expect(rootNode.params[1].value).toBe('10m'); @@ -171,8 +171,8 @@ describe('when parsing', function() { }); it('should parse metric expression with ip number segments', function() { - var parser = new Parser('5.10.123.5'); - var rootNode = parser.getAst(); + const parser = new Parser('5.10.123.5'); + const rootNode = parser.getAst(); expect(rootNode.segments[0].value).toBe('5'); expect(rootNode.segments[1].value).toBe('10'); expect(rootNode.segments[2].value).toBe('123'); diff --git a/public/app/plugins/datasource/influxdb/specs/influx_query.test.ts b/public/app/plugins/datasource/influxdb/specs/influx_query.test.ts index 7c354e8aeeb..a62d5384ac6 100644 --- a/public/app/plugins/datasource/influxdb/specs/influx_query.test.ts +++ b/public/app/plugins/datasource/influxdb/specs/influx_query.test.ts @@ -1,11 +1,11 @@ import InfluxQuery from '../influx_query'; describe('InfluxQuery', function() { - var templateSrv = { replace: val => val }; + const templateSrv = { replace: val => val }; describe('render series with mesurement only', function() { it('should generate correct query', function() { - var query = new InfluxQuery( + const query = new InfluxQuery( { measurement: 'cpu', }, @@ -13,14 +13,14 @@ describe('InfluxQuery', function() { {} ); - var queryText = query.render(); + const queryText = query.render(); expect(queryText).toBe('SELECT mean("value") FROM "cpu" WHERE $timeFilter GROUP BY time($__interval) fill(null)'); }); }); describe('render series with policy only', function() { it('should generate correct query', function() { - var query = new InfluxQuery( + const query = new InfluxQuery( { measurement: 'cpu', policy: '5m_avg', @@ -29,7 +29,7 @@ describe('InfluxQuery', function() { {} ); - var queryText = query.render(); + const queryText = query.render(); expect(queryText).toBe( 'SELECT mean("value") FROM "5m_avg"."cpu" WHERE $timeFilter GROUP BY time($__interval) fill(null)' ); @@ -38,7 +38,7 @@ describe('InfluxQuery', function() { describe('render series with math and alias', function() { it('should generate correct query', function() { - var query = new InfluxQuery( + const query = new InfluxQuery( { measurement: 'cpu', select: [ @@ -54,7 +54,7 @@ describe('InfluxQuery', function() { {} ); - var queryText = query.render(); + const queryText = query.render(); expect(queryText).toBe( 'SELECT mean("value") /100 AS "text" FROM "cpu" WHERE $timeFilter GROUP BY time($__interval) fill(null)' ); @@ -63,7 +63,7 @@ describe('InfluxQuery', function() { describe('series with single tag only', function() { it('should generate correct query', function() { - var query = new InfluxQuery( + const query = new InfluxQuery( { measurement: 'cpu', groupBy: [{ type: 'time', params: ['auto'] }], @@ -73,7 +73,7 @@ describe('InfluxQuery', function() { {} ); - var queryText = query.render(); + const queryText = query.render(); expect(queryText).toBe( 'SELECT mean("value") FROM "cpu" WHERE ("hostname" = \'server\\\\1\') AND $timeFilter' + @@ -82,7 +82,7 @@ describe('InfluxQuery', function() { }); it('should switch regex operator with tag value is regex', function() { - var query = new InfluxQuery( + const query = new InfluxQuery( { measurement: 'cpu', groupBy: [{ type: 'time', params: ['auto'] }], @@ -92,7 +92,7 @@ describe('InfluxQuery', function() { {} ); - var queryText = query.render(); + const queryText = query.render(); expect(queryText).toBe( 'SELECT mean("value") FROM "cpu" WHERE ("app" =~ /e.*/) AND $timeFilter GROUP BY time($__interval)' ); @@ -101,7 +101,7 @@ describe('InfluxQuery', function() { describe('series with multiple tags only', function() { it('should generate correct query', function() { - var query = new InfluxQuery( + const query = new InfluxQuery( { measurement: 'cpu', groupBy: [{ type: 'time', params: ['auto'] }], @@ -111,7 +111,7 @@ describe('InfluxQuery', function() { {} ); - var queryText = query.render(); + const queryText = query.render(); expect(queryText).toBe( 'SELECT mean("value") FROM "cpu" WHERE ("hostname" = \'server1\' AND "app" = \'email\') AND ' + '$timeFilter GROUP BY time($__interval)' @@ -121,7 +121,7 @@ describe('InfluxQuery', function() { describe('series with tags OR condition', function() { it('should generate correct query', function() { - var query = new InfluxQuery( + const query = new InfluxQuery( { measurement: 'cpu', groupBy: [{ type: 'time', params: ['auto'] }], @@ -131,7 +131,7 @@ describe('InfluxQuery', function() { {} ); - var queryText = query.render(); + const queryText = query.render(); expect(queryText).toBe( 'SELECT mean("value") FROM "cpu" WHERE ("hostname" = \'server1\' OR "hostname" = \'server2\') AND ' + '$timeFilter GROUP BY time($__interval)' @@ -141,7 +141,7 @@ describe('InfluxQuery', function() { describe('query with value condition', function() { it('should not quote value', function() { - var query = new InfluxQuery( + const query = new InfluxQuery( { measurement: 'cpu', groupBy: [], @@ -151,14 +151,14 @@ describe('InfluxQuery', function() { {} ); - var queryText = query.render(); + const queryText = query.render(); expect(queryText).toBe('SELECT mean("value") FROM "cpu" WHERE ("value" > 5) AND $timeFilter'); }); }); describe('series with groupByTag', function() { it('should generate correct query', function() { - var query = new InfluxQuery( + const query = new InfluxQuery( { measurement: 'cpu', tags: [], @@ -168,14 +168,14 @@ describe('InfluxQuery', function() { {} ); - var queryText = query.render(); + const queryText = query.render(); expect(queryText).toBe('SELECT mean("value") FROM "cpu" WHERE $timeFilter GROUP BY time($__interval), "host"'); }); }); describe('render series without group by', function() { it('should generate correct query', function() { - var query = new InfluxQuery( + const query = new InfluxQuery( { measurement: 'cpu', select: [[{ type: 'field', params: ['value'] }]], @@ -184,14 +184,14 @@ describe('InfluxQuery', function() { templateSrv, {} ); - var queryText = query.render(); + const queryText = query.render(); expect(queryText).toBe('SELECT "value" FROM "cpu" WHERE $timeFilter'); }); }); describe('render series without group by and fill', function() { it('should generate correct query', function() { - var query = new InfluxQuery( + const query = new InfluxQuery( { measurement: 'cpu', select: [[{ type: 'field', params: ['value'] }]], @@ -200,14 +200,14 @@ describe('InfluxQuery', function() { templateSrv, {} ); - var queryText = query.render(); + const queryText = query.render(); expect(queryText).toBe('SELECT "value" FROM "cpu" WHERE $timeFilter GROUP BY time($__interval) fill(0)'); }); }); describe('when adding group by part', function() { it('should add tag before fill', function() { - var query = new InfluxQuery( + const query = new InfluxQuery( { measurement: 'cpu', groupBy: [{ type: 'time' }, { type: 'fill' }], @@ -224,7 +224,7 @@ describe('InfluxQuery', function() { }); it('should add tag last if no fill', function() { - var query = new InfluxQuery( + const query = new InfluxQuery( { measurement: 'cpu', groupBy: [], @@ -241,7 +241,7 @@ describe('InfluxQuery', function() { describe('when adding select part', function() { it('should add mean after after field', function() { - var query = new InfluxQuery( + const query = new InfluxQuery( { measurement: 'cpu', select: [[{ type: 'field', params: ['value'] }]], @@ -256,7 +256,7 @@ describe('InfluxQuery', function() { }); it('should replace sum by mean', function() { - var query = new InfluxQuery( + const query = new InfluxQuery( { measurement: 'cpu', select: [[{ type: 'field', params: ['value'] }, { type: 'mean' }]], @@ -271,7 +271,7 @@ describe('InfluxQuery', function() { }); it('should add math before alias', function() { - var query = new InfluxQuery( + const query = new InfluxQuery( { measurement: 'cpu', select: [[{ type: 'field', params: ['value'] }, { type: 'mean' }, { type: 'alias' }]], @@ -286,7 +286,7 @@ describe('InfluxQuery', function() { }); it('should add math last', function() { - var query = new InfluxQuery( + const query = new InfluxQuery( { measurement: 'cpu', select: [[{ type: 'field', params: ['value'] }, { type: 'mean' }]], @@ -301,7 +301,7 @@ describe('InfluxQuery', function() { }); it('should replace math', function() { - var query = new InfluxQuery( + const query = new InfluxQuery( { measurement: 'cpu', select: [[{ type: 'field', params: ['value'] }, { type: 'mean' }, { type: 'math' }]], @@ -316,7 +316,7 @@ describe('InfluxQuery', function() { }); it('should add math when one only query part', function() { - var query = new InfluxQuery( + const query = new InfluxQuery( { measurement: 'cpu', select: [[{ type: 'field', params: ['value'] }]], @@ -332,9 +332,9 @@ describe('InfluxQuery', function() { describe('when render adhoc filters', function() { it('should generate correct query segment', function() { - var query = new InfluxQuery({ measurement: 'cpu' }, templateSrv, {}); + const query = new InfluxQuery({ measurement: 'cpu' }, templateSrv, {}); - var queryText = query.renderAdhocFilters([ + const queryText = query.renderAdhocFilters([ { key: 'key1', operator: '=', value: 'value1' }, { key: 'key2', operator: '!=', value: 'value2' }, ]); diff --git a/public/app/plugins/datasource/influxdb/specs/influx_series.test.ts b/public/app/plugins/datasource/influxdb/specs/influx_series.test.ts index 8c8fee9ab9f..bb20db1ba76 100644 --- a/public/app/plugins/datasource/influxdb/specs/influx_series.test.ts +++ b/public/app/plugins/datasource/influxdb/specs/influx_series.test.ts @@ -2,7 +2,7 @@ import InfluxSeries from '../influx_series'; describe('when generating timeseries from influxdb response', function() { describe('given multiple fields for series', function() { - var options = { + const options = { alias: '', series: [ { @@ -15,8 +15,8 @@ describe('when generating timeseries from influxdb response', function() { }; describe('and no alias', function() { it('should generate multiple datapoints for each column', function() { - var series = new InfluxSeries(options); - var result = series.getTimeSeries(); + const series = new InfluxSeries(options); + const result = series.getTimeSeries(); expect(result.length).toBe(3); expect(result[0].target).toBe('cpu.mean {app: test, server: server1}'); @@ -42,8 +42,8 @@ describe('when generating timeseries from influxdb response', function() { describe('and simple alias', function() { it('should use alias', function() { options.alias = 'new series'; - var series = new InfluxSeries(options); - var result = series.getTimeSeries(); + const series = new InfluxSeries(options); + const result = series.getTimeSeries(); expect(result[0].target).toBe('new series'); expect(result[1].target).toBe('new series'); @@ -54,8 +54,8 @@ describe('when generating timeseries from influxdb response', function() { describe('and alias patterns', function() { it('should replace patterns', function() { options.alias = 'alias: $m -> $tag_server ([[measurement]])'; - var series = new InfluxSeries(options); - var result = series.getTimeSeries(); + const series = new InfluxSeries(options); + const result = series.getTimeSeries(); expect(result[0].target).toBe('alias: cpu -> server1 (cpu)'); expect(result[1].target).toBe('alias: cpu -> server1 (cpu)'); @@ -65,7 +65,7 @@ describe('when generating timeseries from influxdb response', function() { }); describe('given measurement with default fieldname', function() { - var options = { + const options = { series: [ { name: 'cpu', @@ -84,8 +84,8 @@ describe('when generating timeseries from influxdb response', function() { describe('and no alias', function() { it('should generate label with no field', function() { - var series = new InfluxSeries(options); - var result = series.getTimeSeries(); + const series = new InfluxSeries(options); + const result = series.getTimeSeries(); expect(result[0].target).toBe('cpu {app: test, server: server1}'); expect(result[1].target).toBe('cpu {app: test2, server: server2}'); @@ -94,7 +94,7 @@ describe('when generating timeseries from influxdb response', function() { }); describe('given two series', function() { - var options = { + const options = { alias: '', series: [ { @@ -114,8 +114,8 @@ describe('when generating timeseries from influxdb response', function() { describe('and no alias', function() { it('should generate two time series', function() { - var series = new InfluxSeries(options); - var result = series.getTimeSeries(); + const series = new InfluxSeries(options); + const result = series.getTimeSeries(); expect(result.length).toBe(2); expect(result[0].target).toBe('cpu.mean {app: test, server: server1}'); @@ -135,8 +135,8 @@ describe('when generating timeseries from influxdb response', function() { describe('and simple alias', function() { it('should use alias', function() { options.alias = 'new series'; - var series = new InfluxSeries(options); - var result = series.getTimeSeries(); + const series = new InfluxSeries(options); + const result = series.getTimeSeries(); expect(result[0].target).toBe('new series'); }); @@ -145,8 +145,8 @@ describe('when generating timeseries from influxdb response', function() { describe('and alias patterns', function() { it('should replace patterns', function() { options.alias = 'alias: $m -> $tag_server ([[measurement]])'; - var series = new InfluxSeries(options); - var result = series.getTimeSeries(); + const series = new InfluxSeries(options); + const result = series.getTimeSeries(); expect(result[0].target).toBe('alias: cpu -> server1 (cpu)'); expect(result[1].target).toBe('alias: cpu -> server2 (cpu)'); @@ -155,7 +155,7 @@ describe('when generating timeseries from influxdb response', function() { }); describe('given measurement with dots', function() { - var options = { + const options = { alias: '', series: [ { @@ -169,15 +169,15 @@ describe('when generating timeseries from influxdb response', function() { it('should replace patterns', function() { options.alias = 'alias: $1 -> [[3]]'; - var series = new InfluxSeries(options); - var result = series.getTimeSeries(); + const series = new InfluxSeries(options); + const result = series.getTimeSeries(); expect(result[0].target).toBe('alias: prod -> count'); }); }); describe('given table response', function() { - var options = { + const options = { alias: '', series: [ { @@ -190,8 +190,8 @@ describe('when generating timeseries from influxdb response', function() { }; it('should return table', function() { - var series = new InfluxSeries(options); - var table = series.getTable(); + const series = new InfluxSeries(options); + const table = series.getTable(); expect(table.type).toBe('table'); expect(table.columns.length).toBe(5); @@ -201,7 +201,7 @@ describe('when generating timeseries from influxdb response', function() { }); describe('given table response from SHOW CARDINALITY', function() { - var options = { + const options = { alias: '', series: [ { @@ -213,8 +213,8 @@ describe('when generating timeseries from influxdb response', function() { }; it('should return table', function() { - var series = new InfluxSeries(options); - var table = series.getTable(); + const series = new InfluxSeries(options); + const table = series.getTable(); expect(table.type).toBe('table'); expect(table.columns.length).toBe(1); @@ -225,7 +225,7 @@ describe('when generating timeseries from influxdb response', function() { describe('given annotation response', function() { describe('with empty tagsColumn', function() { - var options = { + const options = { alias: '', annotation: {}, series: [ @@ -239,15 +239,15 @@ describe('when generating timeseries from influxdb response', function() { }; it('should multiple tags', function() { - var series = new InfluxSeries(options); - var annotations = series.getAnnotations(); + const series = new InfluxSeries(options); + const annotations = series.getAnnotations(); expect(annotations[0].tags.length).toBe(0); }); }); describe('given annotation response', function() { - var options = { + const options = { alias: '', annotation: { tagsColumn: 'datacenter, source', @@ -263,8 +263,8 @@ describe('when generating timeseries from influxdb response', function() { }; it('should multiple tags', function() { - var series = new InfluxSeries(options); - var annotations = series.getAnnotations(); + const series = new InfluxSeries(options); + const annotations = series.getAnnotations(); expect(annotations[0].tags.length).toBe(2); expect(annotations[0].tags[0]).toBe('America'); diff --git a/public/app/plugins/datasource/influxdb/specs/query_builder.test.ts b/public/app/plugins/datasource/influxdb/specs/query_builder.test.ts index 30a9343f56e..d8b27f8b1bf 100644 --- a/public/app/plugins/datasource/influxdb/specs/query_builder.test.ts +++ b/public/app/plugins/datasource/influxdb/specs/query_builder.test.ts @@ -3,139 +3,139 @@ import { InfluxQueryBuilder } from '../query_builder'; describe('InfluxQueryBuilder', function() { describe('when building explore queries', function() { it('should only have measurement condition in tag keys query given query with measurement', function() { - var builder = new InfluxQueryBuilder({ measurement: 'cpu', tags: [] }); - var query = builder.buildExploreQuery('TAG_KEYS'); + const builder = new InfluxQueryBuilder({ measurement: 'cpu', tags: [] }); + const query = builder.buildExploreQuery('TAG_KEYS'); expect(query).toBe('SHOW TAG KEYS FROM "cpu"'); }); it('should handle regex measurement in tag keys query', function() { - var builder = new InfluxQueryBuilder({ + const builder = new InfluxQueryBuilder({ measurement: '/.*/', tags: [], }); - var query = builder.buildExploreQuery('TAG_KEYS'); + const query = builder.buildExploreQuery('TAG_KEYS'); expect(query).toBe('SHOW TAG KEYS FROM /.*/'); }); it('should have no conditions in tags keys query given query with no measurement or tag', function() { - var builder = new InfluxQueryBuilder({ measurement: '', tags: [] }); - var query = builder.buildExploreQuery('TAG_KEYS'); + const builder = new InfluxQueryBuilder({ measurement: '', tags: [] }); + const query = builder.buildExploreQuery('TAG_KEYS'); expect(query).toBe('SHOW TAG KEYS'); }); it('should have where condition in tag keys query with tags', function() { - var builder = new InfluxQueryBuilder({ + const builder = new InfluxQueryBuilder({ measurement: '', tags: [{ key: 'host', value: 'se1' }], }); - var query = builder.buildExploreQuery('TAG_KEYS'); + const query = builder.buildExploreQuery('TAG_KEYS'); expect(query).toBe('SHOW TAG KEYS WHERE "host" = \'se1\''); }); it('should have no conditions in measurement query for query with no tags', function() { - var builder = new InfluxQueryBuilder({ measurement: '', tags: [] }); - var query = builder.buildExploreQuery('MEASUREMENTS'); + const builder = new InfluxQueryBuilder({ measurement: '', tags: [] }); + const query = builder.buildExploreQuery('MEASUREMENTS'); expect(query).toBe('SHOW MEASUREMENTS LIMIT 100'); }); it('should have no conditions in measurement query for query with no tags and empty query', function() { - var builder = new InfluxQueryBuilder({ measurement: '', tags: [] }); - var query = builder.buildExploreQuery('MEASUREMENTS', undefined, ''); + const builder = new InfluxQueryBuilder({ measurement: '', tags: [] }); + const query = builder.buildExploreQuery('MEASUREMENTS', undefined, ''); expect(query).toBe('SHOW MEASUREMENTS LIMIT 100'); }); it('should have WITH MEASUREMENT in measurement query for non-empty query with no tags', function() { - var builder = new InfluxQueryBuilder({ measurement: '', tags: [] }); - var query = builder.buildExploreQuery('MEASUREMENTS', undefined, 'something'); + const builder = new InfluxQueryBuilder({ measurement: '', tags: [] }); + const query = builder.buildExploreQuery('MEASUREMENTS', undefined, 'something'); expect(query).toBe('SHOW MEASUREMENTS WITH MEASUREMENT =~ /something/ LIMIT 100'); }); it('should have WITH MEASUREMENT WHERE in measurement query for non-empty query with tags', function() { - var builder = new InfluxQueryBuilder({ + const builder = new InfluxQueryBuilder({ measurement: '', tags: [{ key: 'app', value: 'email' }], }); - var query = builder.buildExploreQuery('MEASUREMENTS', undefined, 'something'); + const query = builder.buildExploreQuery('MEASUREMENTS', undefined, 'something'); expect(query).toBe('SHOW MEASUREMENTS WITH MEASUREMENT =~ /something/ WHERE "app" = \'email\' LIMIT 100'); }); it('should have where condition in measurement query for query with tags', function() { - var builder = new InfluxQueryBuilder({ + const builder = new InfluxQueryBuilder({ measurement: '', tags: [{ key: 'app', value: 'email' }], }); - var query = builder.buildExploreQuery('MEASUREMENTS'); + const query = builder.buildExploreQuery('MEASUREMENTS'); expect(query).toBe('SHOW MEASUREMENTS WHERE "app" = \'email\' LIMIT 100'); }); it('should have where tag name IN filter in tag values query for query with one tag', function() { - var builder = new InfluxQueryBuilder({ + const builder = new InfluxQueryBuilder({ measurement: '', tags: [{ key: 'app', value: 'asdsadsad' }], }); - var query = builder.buildExploreQuery('TAG_VALUES', 'app'); + const query = builder.buildExploreQuery('TAG_VALUES', 'app'); expect(query).toBe('SHOW TAG VALUES WITH KEY = "app"'); }); it('should have measurement tag condition and tag name IN filter in tag values query', function() { - var builder = new InfluxQueryBuilder({ + const builder = new InfluxQueryBuilder({ measurement: 'cpu', tags: [{ key: 'app', value: 'email' }, { key: 'host', value: 'server1' }], }); - var query = builder.buildExploreQuery('TAG_VALUES', 'app'); + const query = builder.buildExploreQuery('TAG_VALUES', 'app'); expect(query).toBe('SHOW TAG VALUES FROM "cpu" WITH KEY = "app" WHERE "host" = \'server1\''); }); it('should select from policy correctly if policy is specified', function() { - var builder = new InfluxQueryBuilder({ + const builder = new InfluxQueryBuilder({ measurement: 'cpu', policy: 'one_week', tags: [{ key: 'app', value: 'email' }, { key: 'host', value: 'server1' }], }); - var query = builder.buildExploreQuery('TAG_VALUES', 'app'); + const query = builder.buildExploreQuery('TAG_VALUES', 'app'); expect(query).toBe('SHOW TAG VALUES FROM "one_week"."cpu" WITH KEY = "app" WHERE "host" = \'server1\''); }); it('should not include policy when policy is default', function() { - var builder = new InfluxQueryBuilder({ + const builder = new InfluxQueryBuilder({ measurement: 'cpu', policy: 'default', tags: [], }); - var query = builder.buildExploreQuery('TAG_VALUES', 'app'); + const query = builder.buildExploreQuery('TAG_VALUES', 'app'); expect(query).toBe('SHOW TAG VALUES FROM "cpu" WITH KEY = "app"'); }); it('should switch to regex operator in tag condition', function() { - var builder = new InfluxQueryBuilder({ + const builder = new InfluxQueryBuilder({ measurement: 'cpu', tags: [{ key: 'host', value: '/server.*/' }], }); - var query = builder.buildExploreQuery('TAG_VALUES', 'app'); + const query = builder.buildExploreQuery('TAG_VALUES', 'app'); expect(query).toBe('SHOW TAG VALUES FROM "cpu" WITH KEY = "app" WHERE "host" =~ /server.*/'); }); it('should build show field query', function() { - var builder = new InfluxQueryBuilder({ + const builder = new InfluxQueryBuilder({ measurement: 'cpu', tags: [{ key: 'app', value: 'email' }], }); - var query = builder.buildExploreQuery('FIELDS'); + const query = builder.buildExploreQuery('FIELDS'); expect(query).toBe('SHOW FIELD KEYS FROM "cpu"'); }); it('should build show field query with regexp', function() { - var builder = new InfluxQueryBuilder({ + const builder = new InfluxQueryBuilder({ measurement: '/$var/', tags: [{ key: 'app', value: 'email' }], }); - var query = builder.buildExploreQuery('FIELDS'); + const query = builder.buildExploreQuery('FIELDS'); expect(query).toBe('SHOW FIELD KEYS FROM /$var/'); }); it('should build show retention policies query', function() { - var builder = new InfluxQueryBuilder({ measurement: 'cpu', tags: [] }, 'site'); - var query = builder.buildExploreQuery('RETENTION POLICIES'); + const builder = new InfluxQueryBuilder({ measurement: 'cpu', tags: [] }, 'site'); + const query = builder.buildExploreQuery('RETENTION POLICIES'); expect(query).toBe('SHOW RETENTION POLICIES on "site"'); }); }); diff --git a/public/app/plugins/datasource/influxdb/specs/query_part.test.ts b/public/app/plugins/datasource/influxdb/specs/query_part.test.ts index e9e6d216c1e..264c695c8a7 100644 --- a/public/app/plugins/datasource/influxdb/specs/query_part.test.ts +++ b/public/app/plugins/datasource/influxdb/specs/query_part.test.ts @@ -3,7 +3,7 @@ import queryPart from '../query_part'; describe('InfluxQueryPart', () => { describe('series with measurement only', () => { it('should handle nested function parts', () => { - var part = queryPart.create({ + const part = queryPart.create({ type: 'derivative', params: ['10s'], }); @@ -13,7 +13,7 @@ describe('InfluxQueryPart', () => { }); it('should nest spread function', () => { - var part = queryPart.create({ + const part = queryPart.create({ type: 'spread', }); @@ -22,7 +22,7 @@ describe('InfluxQueryPart', () => { }); it('should handle suffix parts', () => { - var part = queryPart.create({ + const part = queryPart.create({ type: 'math', params: ['/ 100'], }); @@ -32,7 +32,7 @@ describe('InfluxQueryPart', () => { }); it('should handle alias parts', () => { - var part = queryPart.create({ + const part = queryPart.create({ type: 'alias', params: ['test'], }); @@ -42,7 +42,7 @@ describe('InfluxQueryPart', () => { }); it('should nest distinct when count is selected', () => { - var selectParts = [ + const selectParts = [ queryPart.create({ type: 'field', category: queryPart.getCategories().Fields, @@ -52,7 +52,7 @@ describe('InfluxQueryPart', () => { category: queryPart.getCategories().Aggregations, }), ]; - var partModel = queryPart.create({ + const partModel = queryPart.create({ type: 'distinct', category: queryPart.getCategories().Aggregations, }); @@ -64,7 +64,7 @@ describe('InfluxQueryPart', () => { }); it('should convert to count distinct when distinct is selected and count added', () => { - var selectParts = [ + const selectParts = [ queryPart.create({ type: 'field', category: queryPart.getCategories().Fields, @@ -74,7 +74,7 @@ describe('InfluxQueryPart', () => { category: queryPart.getCategories().Aggregations, }), ]; - var partModel = queryPart.create({ + const partModel = queryPart.create({ type: 'count', category: queryPart.getCategories().Aggregations, }); @@ -86,7 +86,7 @@ describe('InfluxQueryPart', () => { }); it('should replace count distinct if an aggregation is selected', () => { - var selectParts = [ + const selectParts = [ queryPart.create({ type: 'field', category: queryPart.getCategories().Fields, @@ -100,7 +100,7 @@ describe('InfluxQueryPart', () => { category: queryPart.getCategories().Aggregations, }), ]; - var partModel = queryPart.create({ + const partModel = queryPart.create({ type: 'mean', category: queryPart.getCategories().Selectors, }); @@ -112,7 +112,7 @@ describe('InfluxQueryPart', () => { }); it('should not allowed nested counts when count distinct is selected', () => { - var selectParts = [ + const selectParts = [ queryPart.create({ type: 'field', category: queryPart.getCategories().Fields, @@ -126,7 +126,7 @@ describe('InfluxQueryPart', () => { category: queryPart.getCategories().Aggregations, }), ]; - var partModel = queryPart.create({ + const partModel = queryPart.create({ type: 'count', category: queryPart.getCategories().Aggregations, }); @@ -139,7 +139,7 @@ describe('InfluxQueryPart', () => { }); it('should not remove count distinct when distinct is added', () => { - var selectParts = [ + const selectParts = [ queryPart.create({ type: 'field', category: queryPart.getCategories().Fields, @@ -153,7 +153,7 @@ describe('InfluxQueryPart', () => { category: queryPart.getCategories().Aggregations, }), ]; - var partModel = queryPart.create({ + const partModel = queryPart.create({ type: 'distinct', category: queryPart.getCategories().Aggregations, }); @@ -166,7 +166,7 @@ describe('InfluxQueryPart', () => { }); it('should remove distinct when sum aggregation is selected', () => { - var selectParts = [ + const selectParts = [ queryPart.create({ type: 'field', category: queryPart.getCategories().Fields, @@ -176,7 +176,7 @@ describe('InfluxQueryPart', () => { category: queryPart.getCategories().Aggregations, }), ]; - var partModel = queryPart.create({ + const partModel = queryPart.create({ type: 'sum', category: queryPart.getCategories().Aggregations, }); diff --git a/public/app/plugins/datasource/influxdb/specs/response_parser.test.ts b/public/app/plugins/datasource/influxdb/specs/response_parser.test.ts index 525508b2c1d..cca78974fe3 100644 --- a/public/app/plugins/datasource/influxdb/specs/response_parser.test.ts +++ b/public/app/plugins/datasource/influxdb/specs/response_parser.test.ts @@ -5,8 +5,8 @@ describe('influxdb response parser', () => { const parser = new ResponseParser(); describe('SHOW TAG response', () => { - var query = 'SHOW TAG KEYS FROM "cpu"'; - var response = { + const query = 'SHOW TAG KEYS FROM "cpu"'; + const response = { results: [ { series: [ @@ -20,7 +20,7 @@ describe('influxdb response parser', () => { ], }; - var result = parser.parse(query, response); + const result = parser.parse(query, response); it('expects three results', () => { expect(_.size(result)).toBe(3); @@ -28,10 +28,10 @@ describe('influxdb response parser', () => { }); describe('SHOW TAG VALUES response', () => { - var query = 'SHOW TAG VALUES FROM "cpu" WITH KEY = "hostname"'; + const query = 'SHOW TAG VALUES FROM "cpu" WITH KEY = "hostname"'; describe('response from 0.10.0', () => { - var response = { + const response = { results: [ { series: [ @@ -45,7 +45,7 @@ describe('influxdb response parser', () => { ], }; - var result = parser.parse(query, response); + const result = parser.parse(query, response); it('should get two responses', () => { expect(_.size(result)).toBe(2); @@ -55,7 +55,7 @@ describe('influxdb response parser', () => { }); describe('response from 0.12.0', () => { - var response = { + const response = { results: [ { series: [ @@ -74,7 +74,7 @@ describe('influxdb response parser', () => { ], }; - var result = parser.parse(query, response); + const result = parser.parse(query, response); it('should get two responses', () => { expect(_.size(result)).toBe(3); @@ -86,8 +86,8 @@ describe('influxdb response parser', () => { }); describe('SELECT response', () => { - var query = 'SELECT "usage_iowait" FROM "cpu" LIMIT 10'; - var response = { + const query = 'SELECT "usage_iowait" FROM "cpu" LIMIT 10'; + const response = { results: [ { series: [ @@ -101,7 +101,7 @@ describe('influxdb response parser', () => { ], }; - var result = parser.parse(query, response); + const result = parser.parse(query, response); it('should return second column', () => { expect(_.size(result)).toBe(3); @@ -112,10 +112,10 @@ describe('influxdb response parser', () => { }); describe('SHOW FIELD response', () => { - var query = 'SHOW FIELD KEYS FROM "cpu"'; + const query = 'SHOW FIELD KEYS FROM "cpu"'; describe('response from pre-1.0', () => { - var response = { + const response = { results: [ { series: [ @@ -129,7 +129,7 @@ describe('influxdb response parser', () => { ], }; - var result = parser.parse(query, response); + const result = parser.parse(query, response); it('should get two responses', () => { expect(_.size(result)).toBe(1); @@ -137,7 +137,7 @@ describe('influxdb response parser', () => { }); describe('response from 1.0', () => { - var response = { + const response = { results: [ { series: [ @@ -151,7 +151,7 @@ describe('influxdb response parser', () => { ], }; - var result = parser.parse(query, response); + const result = parser.parse(query, response); it('should return first column', () => { expect(_.size(result)).toBe(1); diff --git a/public/app/plugins/datasource/opentsdb/specs/datasource.test.ts b/public/app/plugins/datasource/opentsdb/specs/datasource.test.ts index befa39fc80e..e7e53c0dd5b 100644 --- a/public/app/plugins/datasource/opentsdb/specs/datasource.test.ts +++ b/public/app/plugins/datasource/opentsdb/specs/datasource.test.ts @@ -16,8 +16,8 @@ describe('opentsdb', () => { }); describe('When performing metricFindQuery', () => { - var results; - var requestOptions; + let results; + let requestOptions; beforeEach(async () => { ctx.backendSrv.datasourceRequest = await function(options) { diff --git a/public/app/plugins/datasource/opentsdb/specs/query_ctrl.test.ts b/public/app/plugins/datasource/opentsdb/specs/query_ctrl.test.ts index 58a10b21207..6fdcd29aecd 100644 --- a/public/app/plugins/datasource/opentsdb/specs/query_ctrl.test.ts +++ b/public/app/plugins/datasource/opentsdb/specs/query_ctrl.test.ts @@ -1,7 +1,7 @@ import { OpenTsQueryCtrl } from '../query_ctrl'; describe('OpenTsQueryCtrl', () => { - var ctx = { + const ctx = { target: { target: '' }, datasource: { tsdbVersion: '', diff --git a/public/app/plugins/datasource/prometheus/specs/datasource.test.ts b/public/app/plugins/datasource/prometheus/specs/datasource.test.ts index fd963f7986e..846a00212d0 100644 --- a/public/app/plugins/datasource/prometheus/specs/datasource.test.ts +++ b/public/app/plugins/datasource/prometheus/specs/datasource.test.ts @@ -409,14 +409,14 @@ const timeSrv = { describe('PrometheusDatasource', () => { describe('When querying prometheus with one target using query editor target spec', async () => { - var results; - var query = { + let results; + const query = { range: { from: time({ seconds: 63 }), to: time({ seconds: 183 }) }, targets: [{ expr: 'test{job="testjob"}', format: 'time_series' }], interval: '60s', }; // Interval alignment with step - var urlExpected = + const urlExpected = 'proxied/api/v1/query_range?query=' + encodeURIComponent('test{job="testjob"}') + '&start=60&end=240&step=60'; beforeEach(async () => { @@ -453,12 +453,12 @@ describe('PrometheusDatasource', () => { }); }); describe('When querying prometheus with one target which return multiple series', () => { - var results; - var start = 60; - var end = 360; - var step = 60; + let results; + const start = 60; + const end = 360; + const step = 60; - var query = { + const query = { range: { from: time({ seconds: start }), to: time({ seconds: end }) }, targets: [{ expr: 'test{job="testjob"}', format: 'time_series' }], interval: '60s', @@ -505,7 +505,7 @@ describe('PrometheusDatasource', () => { expect(results.data[0].datapoints[1][0]).toBe(3846); }); it('should fill null after last datapoint in response', () => { - var length = (end - start) / step + 1; + const length = (end - start) / step + 1; expect(results.data[0].datapoints[length - 2][1]).toBe((end - step * 1) * 1000); expect(results.data[0].datapoints[length - 2][0]).toBe(3848); expect(results.data[0].datapoints[length - 1][1]).toBe(end * 1000); @@ -521,9 +521,9 @@ describe('PrometheusDatasource', () => { }); }); describe('When querying prometheus with one target and instant = true', () => { - var results; - var urlExpected = 'proxied/api/v1/query?query=' + encodeURIComponent('test{job="testjob"}') + '&time=123'; - var query = { + let results; + const urlExpected = 'proxied/api/v1/query?query=' + encodeURIComponent('test{job="testjob"}') + '&time=123'; + const query = { range: { from: time({ seconds: 63 }), to: time({ seconds: 123 }) }, targets: [{ expr: 'test{job="testjob"}', format: 'time_series', instant: true }], interval: '60s', @@ -563,9 +563,9 @@ describe('PrometheusDatasource', () => { }); }); describe('When performing annotationQuery', () => { - var results; + let results; - var options = { + const options = { annotation: { expr: 'ALERTS{alertstate="firing"}', tagKeys: 'job', @@ -617,8 +617,8 @@ describe('PrometheusDatasource', () => { }); describe('When resultFormat is table and instant = true', () => { - var results; - var query = { + let results; + const query = { range: { from: time({ seconds: 63 }), to: time({ seconds: 123 }) }, targets: [{ expr: 'test{job="testjob"}', format: 'time_series', instant: true }], interval: '60s', @@ -653,7 +653,7 @@ describe('PrometheusDatasource', () => { }); describe('The "step" query parameter', () => { - var response = { + const response = { status: 'success', data: { data: { @@ -686,13 +686,13 @@ describe('PrometheusDatasource', () => { }); it('step should never go below 1', async () => { - var query = { + const query = { // 6 minute range range: { from: time({ minutes: 1 }), to: time({ minutes: 7 }) }, targets: [{ expr: 'test' }], interval: '100ms', }; - var urlExpected = 'proxied/api/v1/query_range?query=test&start=60&end=420&step=1'; + const urlExpected = 'proxied/api/v1/query_range?query=test&start=60&end=420&step=1'; backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response)); ctx.ds = new PrometheusDatasource(instanceSettings, q, backendSrv, templateSrv, timeSrv); await ctx.ds.query(query); @@ -702,7 +702,7 @@ describe('PrometheusDatasource', () => { }); it('should be auto interval when greater than min interval', async () => { - var query = { + const query = { // 6 minute range range: { from: time({ minutes: 1 }), to: time({ minutes: 7 }) }, targets: [ @@ -713,7 +713,7 @@ describe('PrometheusDatasource', () => { ], interval: '10s', }; - var urlExpected = 'proxied/api/v1/query_range?query=test&start=60&end=420&step=10'; + const urlExpected = 'proxied/api/v1/query_range?query=test&start=60&end=420&step=10'; backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response)); ctx.ds = new PrometheusDatasource(instanceSettings, q, backendSrv, templateSrv, timeSrv); await ctx.ds.query(query); @@ -722,15 +722,15 @@ describe('PrometheusDatasource', () => { expect(res.url).toBe(urlExpected); }); it('should result in querying fewer than 11000 data points', async () => { - var query = { + const query = { // 6 hour range range: { from: time({ hours: 1 }), to: time({ hours: 7 }) }, targets: [{ expr: 'test' }], interval: '1s', }; - var end = 7 * 60 * 60; - var start = 60 * 60; - var urlExpected = 'proxied/api/v1/query_range?query=test&start=' + start + '&end=' + end + '&step=2'; + const end = 7 * 60 * 60; + const start = 60 * 60; + const urlExpected = 'proxied/api/v1/query_range?query=test&start=' + start + '&end=' + end + '&step=2'; backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response)); ctx.ds = new PrometheusDatasource(instanceSettings, q, backendSrv, templateSrv, timeSrv); await ctx.ds.query(query); @@ -739,7 +739,7 @@ describe('PrometheusDatasource', () => { expect(res.url).toBe(urlExpected); }); it('should not apply min interval when interval * intervalFactor greater', async () => { - var query = { + const query = { // 6 minute range range: { from: time({ minutes: 1 }), to: time({ minutes: 7 }) }, targets: [ @@ -752,7 +752,7 @@ describe('PrometheusDatasource', () => { interval: '5s', }; // times get rounded up to interval - var urlExpected = 'proxied/api/v1/query_range?query=test&start=50&end=450&step=50'; + const urlExpected = 'proxied/api/v1/query_range?query=test&start=50&end=450&step=50'; backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response)); ctx.ds = new PrometheusDatasource(instanceSettings, q, backendSrv, templateSrv, timeSrv); await ctx.ds.query(query); @@ -761,7 +761,7 @@ describe('PrometheusDatasource', () => { expect(res.url).toBe(urlExpected); }); it('should apply min interval when interval * intervalFactor smaller', async () => { - var query = { + const query = { // 6 minute range range: { from: time({ minutes: 1 }), to: time({ minutes: 7 }) }, targets: [ @@ -773,7 +773,7 @@ describe('PrometheusDatasource', () => { ], interval: '5s', }; - var urlExpected = 'proxied/api/v1/query_range?query=test' + '&start=60&end=420&step=15'; + const urlExpected = 'proxied/api/v1/query_range?query=test' + '&start=60&end=420&step=15'; backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response)); ctx.ds = new PrometheusDatasource(instanceSettings, q, backendSrv, templateSrv, timeSrv); await ctx.ds.query(query); @@ -782,7 +782,7 @@ describe('PrometheusDatasource', () => { expect(res.url).toBe(urlExpected); }); it('should apply intervalFactor to auto interval when greater', async () => { - var query = { + const query = { // 6 minute range range: { from: time({ minutes: 1 }), to: time({ minutes: 7 }) }, targets: [ @@ -795,7 +795,7 @@ describe('PrometheusDatasource', () => { interval: '10s', }; // times get aligned to interval - var urlExpected = 'proxied/api/v1/query_range?query=test' + '&start=0&end=500&step=100'; + const urlExpected = 'proxied/api/v1/query_range?query=test' + '&start=0&end=500&step=100'; backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response)); ctx.ds = new PrometheusDatasource(instanceSettings, q, backendSrv, templateSrv, timeSrv); await ctx.ds.query(query); @@ -804,7 +804,7 @@ describe('PrometheusDatasource', () => { expect(res.url).toBe(urlExpected); }); it('should not not be affected by the 11000 data points limit when large enough', async () => { - var query = { + const query = { // 1 week range range: { from: time({}), to: time({ hours: 7 * 24 }) }, targets: [ @@ -815,9 +815,9 @@ describe('PrometheusDatasource', () => { ], interval: '10s', }; - var end = 7 * 24 * 60 * 60; - var start = 0; - var urlExpected = 'proxied/api/v1/query_range?query=test' + '&start=' + start + '&end=' + end + '&step=100'; + const end = 7 * 24 * 60 * 60; + const start = 0; + const urlExpected = 'proxied/api/v1/query_range?query=test' + '&start=' + start + '&end=' + end + '&step=100'; backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response)); ctx.ds = new PrometheusDatasource(instanceSettings, q, backendSrv, templateSrv, timeSrv); await ctx.ds.query(query); @@ -826,7 +826,7 @@ describe('PrometheusDatasource', () => { expect(res.url).toBe(urlExpected); }); it('should be determined by the 11000 data points limit when too small', async () => { - var query = { + const query = { // 1 week range range: { from: time({}), to: time({ hours: 7 * 24 }) }, targets: [ @@ -837,9 +837,9 @@ describe('PrometheusDatasource', () => { ], interval: '5s', }; - var end = 7 * 24 * 60 * 60; - var start = 0; - var urlExpected = 'proxied/api/v1/query_range?query=test' + '&start=' + start + '&end=' + end + '&step=60'; + const end = 7 * 24 * 60 * 60; + const start = 0; + const urlExpected = 'proxied/api/v1/query_range?query=test' + '&start=' + start + '&end=' + end + '&step=60'; backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response)); ctx.ds = new PrometheusDatasource(instanceSettings, q, backendSrv, templateSrv, timeSrv); await ctx.ds.query(query); @@ -850,7 +850,7 @@ describe('PrometheusDatasource', () => { }); describe('The __interval and __interval_ms template variables', () => { - var response = { + const response = { status: 'success', data: { data: { @@ -861,7 +861,7 @@ describe('PrometheusDatasource', () => { }; it('should be unchanged when auto interval is greater than min interval', async () => { - var query = { + const query = { // 6 minute range range: { from: time({ minutes: 1 }), to: time({ minutes: 7 }) }, targets: [ @@ -877,7 +877,7 @@ describe('PrometheusDatasource', () => { }, }; - var urlExpected = + const urlExpected = 'proxied/api/v1/query_range?query=' + encodeURIComponent('rate(test[$__interval])') + '&start=60&end=420&step=10'; @@ -902,7 +902,7 @@ describe('PrometheusDatasource', () => { }); }); it('should be min interval when it is greater than auto interval', async () => { - var query = { + const query = { // 6 minute range range: { from: time({ minutes: 1 }), to: time({ minutes: 7 }) }, targets: [ @@ -917,7 +917,7 @@ describe('PrometheusDatasource', () => { __interval_ms: { text: 5 * 1000, value: 5 * 1000 }, }, }; - var urlExpected = + const urlExpected = 'proxied/api/v1/query_range?query=' + encodeURIComponent('rate(test[$__interval])') + '&start=60&end=420&step=10'; @@ -941,7 +941,7 @@ describe('PrometheusDatasource', () => { }); }); it('should account for intervalFactor', async () => { - var query = { + const query = { // 6 minute range range: { from: time({ minutes: 1 }), to: time({ minutes: 7 }) }, targets: [ @@ -957,7 +957,7 @@ describe('PrometheusDatasource', () => { __interval_ms: { text: 10 * 1000, value: 10 * 1000 }, }, }; - var urlExpected = + const urlExpected = 'proxied/api/v1/query_range?query=' + encodeURIComponent('rate(test[$__interval])') + '&start=0&end=500&step=100'; @@ -986,7 +986,7 @@ describe('PrometheusDatasource', () => { expect(query.scopedVars.__interval_ms.value).toBe(10 * 1000); }); it('should be interval * intervalFactor when greater than min interval', async () => { - var query = { + const query = { // 6 minute range range: { from: time({ minutes: 1 }), to: time({ minutes: 7 }) }, targets: [ @@ -1002,7 +1002,7 @@ describe('PrometheusDatasource', () => { __interval_ms: { text: 5 * 1000, value: 5 * 1000 }, }, }; - var urlExpected = + const urlExpected = 'proxied/api/v1/query_range?query=' + encodeURIComponent('rate(test[$__interval])') + '&start=50&end=450&step=50'; @@ -1027,7 +1027,7 @@ describe('PrometheusDatasource', () => { }); }); it('should be min interval when greater than interval * intervalFactor', async () => { - var query = { + const query = { // 6 minute range range: { from: time({ minutes: 1 }), to: time({ minutes: 7 }) }, targets: [ @@ -1043,7 +1043,7 @@ describe('PrometheusDatasource', () => { __interval_ms: { text: 5 * 1000, value: 5 * 1000 }, }, }; - var urlExpected = + const urlExpected = 'proxied/api/v1/query_range?query=' + encodeURIComponent('rate(test[$__interval])') + '&start=60&end=420&step=15'; @@ -1067,7 +1067,7 @@ describe('PrometheusDatasource', () => { }); }); it('should be determined by the 11000 data points limit, accounting for intervalFactor', async () => { - var query = { + const query = { // 1 week range range: { from: time({}), to: time({ hours: 7 * 24 }) }, targets: [ @@ -1082,9 +1082,9 @@ describe('PrometheusDatasource', () => { __interval_ms: { text: 5 * 1000, value: 5 * 1000 }, }, }; - var end = 7 * 24 * 60 * 60; - var start = 0; - var urlExpected = + const end = 7 * 24 * 60 * 60; + const start = 0; + const urlExpected = 'proxied/api/v1/query_range?query=' + encodeURIComponent('rate(test[$__interval])') + '&start=' + @@ -1115,7 +1115,7 @@ describe('PrometheusDatasource', () => { }); describe('PrometheusDatasource for POST', () => { - // var ctx = new helpers.ServiceTestContext(); + // const ctx = new helpers.ServiceTestContext(); const instanceSettings = { url: 'proxied', directUrl: 'direct', @@ -1125,15 +1125,15 @@ describe('PrometheusDatasource for POST', () => { }; describe('When querying prometheus with one target using query editor target spec', () => { - var results; - var urlExpected = 'proxied/api/v1/query_range'; - var dataExpected = { + let results; + const urlExpected = 'proxied/api/v1/query_range'; + const dataExpected = { query: 'test{job="testjob"}', start: 1 * 60, end: 3 * 60, step: 60, }; - var query = { + const query = { range: { from: time({ minutes: 1, seconds: 3 }), to: time({ minutes: 2, seconds: 3 }) }, targets: [{ expr: 'test{job="testjob"}', format: 'time_series' }], interval: '60s', diff --git a/public/app/plugins/datasource/prometheus/specs/result_transformer.test.ts b/public/app/plugins/datasource/prometheus/specs/result_transformer.test.ts index ac85e1374bb..0ccb79a5d1f 100644 --- a/public/app/plugins/datasource/prometheus/specs/result_transformer.test.ts +++ b/public/app/plugins/datasource/prometheus/specs/result_transformer.test.ts @@ -11,7 +11,7 @@ describe('Prometheus Result Transformer', () => { }); describe('When resultFormat is table', () => { - var response = { + const response = { status: 'success', data: { resultType: 'matrix', @@ -33,7 +33,7 @@ describe('Prometheus Result Transformer', () => { }; it('should return table model', () => { - var table = ctx.resultTransformer.transformMetricDataToTable(response.data.result); + const table = ctx.resultTransformer.transformMetricDataToTable(response.data.result); expect(table.type).toBe('table'); expect(table.rows).toEqual([ [1443454528000, 'test', '', 'testjob', 3846], @@ -49,7 +49,7 @@ describe('Prometheus Result Transformer', () => { }); it('should column title include refId if response count is more than 2', () => { - var table = ctx.resultTransformer.transformMetricDataToTable(response.data.result, 2, 'B'); + const table = ctx.resultTransformer.transformMetricDataToTable(response.data.result, 2, 'B'); expect(table.type).toBe('table'); expect(table.columns).toMatchObject([ { text: 'Time', type: 'time' }, @@ -62,7 +62,7 @@ describe('Prometheus Result Transformer', () => { }); describe('When resultFormat is table and instant = true', () => { - var response = { + const response = { status: 'success', data: { resultType: 'vector', @@ -76,7 +76,7 @@ describe('Prometheus Result Transformer', () => { }; it('should return table model', () => { - var table = ctx.resultTransformer.transformMetricDataToTable(response.data.result); + const table = ctx.resultTransformer.transformMetricDataToTable(response.data.result); expect(table.type).toBe('table'); expect(table.rows).toEqual([[1443454528000, 'test', 'testjob', 3846]]); expect(table.columns).toMatchObject([ @@ -89,7 +89,7 @@ describe('Prometheus Result Transformer', () => { }); describe('When resultFormat is heatmap', () => { - var response = { + const response = { status: 'success', data: { resultType: 'matrix', diff --git a/public/app/plugins/panel/graph/specs/data_processor.test.ts b/public/app/plugins/panel/graph/specs/data_processor.test.ts index 3ae34e277c8..2e8d4eb6eab 100644 --- a/public/app/plugins/panel/graph/specs/data_processor.test.ts +++ b/public/app/plugins/panel/graph/specs/data_processor.test.ts @@ -1,11 +1,11 @@ import { DataProcessor } from '../data_processor'; describe('Graph DataProcessor', function() { - var panel: any = { + const panel: any = { xaxis: {}, }; - var processor = new DataProcessor(panel); + const processor = new DataProcessor(panel); describe('Given default xaxis options and query that returns docs', () => { beforeEach(() => { @@ -29,7 +29,7 @@ describe('Graph DataProcessor', function() { }); describe('getDataFieldNames(', () => { - var dataList = [ + const dataList = [ { type: 'docs', datapoints: [ @@ -46,7 +46,7 @@ describe('Graph DataProcessor', function() { ]; it('Should return all field names', () => { - var fields = processor.getDataFieldNames(dataList, false); + const fields = processor.getDataFieldNames(dataList, false); expect(fields).toContain('hostname'); expect(fields).toContain('valueField'); expect(fields).toContain('nested.prop1'); @@ -54,7 +54,7 @@ describe('Graph DataProcessor', function() { }); it('Should return all number fields', () => { - var fields = processor.getDataFieldNames(dataList, true); + const fields = processor.getDataFieldNames(dataList, true); expect(fields).toContain('valueField'); expect(fields).toContain('nested.value2'); }); diff --git a/public/app/plugins/panel/graph/specs/graph.test.ts b/public/app/plugins/panel/graph/specs/graph.test.ts index 2ae76bb9c9c..64dd1de01ed 100644 --- a/public/app/plugins/panel/graph/specs/graph.test.ts +++ b/public/app/plugins/panel/graph/specs/graph.test.ts @@ -243,7 +243,7 @@ describe('grafanaGraph', function() { }); it('should apply axis transform, autoscaling (if necessary) and ticks', function() { - var axisAutoscale = ctx.plotOptions.yaxes[0]; + const axisAutoscale = ctx.plotOptions.yaxes[0]; expect(axisAutoscale.transform(100)).toBe(2); expect(axisAutoscale.inverseTransform(-3)).toBeCloseTo(0.001); expect(axisAutoscale.min).toBeCloseTo(0.001); @@ -256,7 +256,7 @@ describe('grafanaGraph', function() { expect(axisAutoscale.ticks[axisAutoscale.ticks.length - 1]).toBe(10000); } - var axisFixedscale = ctx.plotOptions.yaxes[1]; + const axisFixedscale = ctx.plotOptions.yaxes[1]; expect(axisFixedscale.min).toBe(0.05); expect(axisFixedscale.max).toBe(1500); expect(axisFixedscale.ticks.length).toBe(5); @@ -278,7 +278,7 @@ describe('grafanaGraph', function() { }); it('should not set min and max and should create some fake ticks', function() { - var axisAutoscale = ctx.plotOptions.yaxes[0]; + const axisAutoscale = ctx.plotOptions.yaxes[0]; expect(axisAutoscale.transform(100)).toBe(2); expect(axisAutoscale.inverseTransform(-3)).toBeCloseTo(0.001); expect(axisAutoscale.min).toBe(undefined); @@ -304,7 +304,7 @@ describe('grafanaGraph', function() { }); }); it('should set min to 0.1 and add a tick for 0.1', function() { - var axisAutoscale = ctx.plotOptions.yaxes[0]; + const axisAutoscale = ctx.plotOptions.yaxes[0]; expect(axisAutoscale.transform(100)).toBe(2); expect(axisAutoscale.inverseTransform(-3)).toBeCloseTo(0.001); expect(axisAutoscale.min).toBe(0.1); @@ -331,7 +331,7 @@ describe('grafanaGraph', function() { }); it('should regenerate ticks so that if fits on the y-axis', function() { - var axisAutoscale = ctx.plotOptions.yaxes[0]; + const axisAutoscale = ctx.plotOptions.yaxes[0]; expect(axisAutoscale.min).toBe(0.1); expect(axisAutoscale.ticks.length).toBe(8); expect(axisAutoscale.ticks[0]).toBe(0.1); @@ -432,7 +432,7 @@ describe('grafanaGraph', function() { }); it('should show percentage', function() { - var axis = ctx.plotOptions.yaxes[0]; + const axis = ctx.plotOptions.yaxes[0]; expect(axis.tickFormatter(100, axis)).toBe('100%'); }); }); @@ -448,7 +448,7 @@ describe('grafanaGraph', function() { }); it('should format dates as hours minutes', function() { - var axis = ctx.plotOptions.xaxis; + const axis = ctx.plotOptions.xaxis; expect(axis.timeformat).toBe('%H:%M'); }); }); @@ -462,7 +462,7 @@ describe('grafanaGraph', function() { }); it('should format dates as month days', function() { - var axis = ctx.plotOptions.xaxis; + const axis = ctx.plotOptions.xaxis; expect(axis.timeformat).toBe('%m/%d'); }); }); diff --git a/public/app/plugins/panel/graph/specs/graph_ctrl.test.ts b/public/app/plugins/panel/graph/specs/graph_ctrl.test.ts index 49efa8d4120..2feb94a5626 100644 --- a/public/app/plugins/panel/graph/specs/graph_ctrl.test.ts +++ b/public/app/plugins/panel/graph/specs/graph_ctrl.test.ts @@ -43,7 +43,7 @@ describe('GraphCtrl', () => { describe('when time series are outside range', () => { beforeEach(() => { - var data = [ + const data = [ { target: 'test.cpu1', datapoints: [[45, 1234567890], [60, 1234567899]], @@ -61,14 +61,14 @@ describe('GraphCtrl', () => { describe('when time series are inside range', () => { beforeEach(() => { - var range = { + const range = { from: moment() .subtract(1, 'days') .valueOf(), to: moment().valueOf(), }; - var data = [ + const data = [ { target: 'test.cpu1', datapoints: [[45, range.from + 1000], [60, range.from + 10000]], @@ -86,7 +86,7 @@ describe('GraphCtrl', () => { describe('datapointsCount given 2 series', () => { beforeEach(() => { - var data = [{ target: 'test.cpu1', datapoints: [] }, { target: 'test.cpu2', datapoints: [] }]; + const data = [{ target: 'test.cpu1', datapoints: [] }, { target: 'test.cpu2', datapoints: [] }]; ctx.ctrl.onDataReceived(data); }); diff --git a/public/app/plugins/panel/graph/specs/graph_tooltip.test.ts b/public/app/plugins/panel/graph/specs/graph_tooltip.test.ts index baebf2c5930..ecc6ce0fb21 100644 --- a/public/app/plugins/panel/graph/specs/graph_tooltip.test.ts +++ b/public/app/plugins/panel/graph/specs/graph_tooltip.test.ts @@ -3,18 +3,18 @@ jest.mock('app/core/core', () => ({})); import $ from 'jquery'; import GraphTooltip from '../graph_tooltip'; -var scope = { +const scope = { appEvent: jest.fn(), onAppEvent: jest.fn(), ctrl: {}, }; -var elem = $('
    '); -var dashboard = {}; -var getSeriesFn; +const elem = $('
    '); +const dashboard = {}; +const getSeriesFn = () => {}; function describeSharedTooltip(desc, fn) { - var ctx: any = {}; + const ctx: any = {}; ctx.ctrl = scope.ctrl; ctx.ctrl.panel = { tooltip: { @@ -31,7 +31,7 @@ function describeSharedTooltip(desc, fn) { describe(desc, function() { beforeEach(function() { ctx.setupFn(); - var tooltip = new GraphTooltip(elem, dashboard, scope, getSeriesFn); + const tooltip = new GraphTooltip(elem, dashboard, scope, getSeriesFn); ctx.results = tooltip.getMultiSeriesPlotHoverInfo(ctx.data, ctx.pos); }); @@ -40,28 +40,28 @@ function describeSharedTooltip(desc, fn) { } describe('findHoverIndexFromData', function() { - var tooltip = new GraphTooltip(elem, dashboard, scope, getSeriesFn); - var series = { + const tooltip = new GraphTooltip(elem, dashboard, scope, getSeriesFn); + const series = { data: [[100, 0], [101, 0], [102, 0], [103, 0], [104, 0], [105, 0], [106, 0], [107, 0]], }; it('should return 0 if posX out of lower bounds', function() { - var posX = 99; + const posX = 99; expect(tooltip.findHoverIndexFromData(posX, series)).toBe(0); }); it('should return n - 1 if posX out of upper bounds', function() { - var posX = 108; + const posX = 108; expect(tooltip.findHoverIndexFromData(posX, series)).toBe(series.data.length - 1); }); it('should return i if posX in series', function() { - var posX = 104; + const posX = 104; expect(tooltip.findHoverIndexFromData(posX, series)).toBe(4); }); it('should return i if posX not in series and i + 1 > posX', function() { - var posX = 104.9; + const posX = 104.9; expect(tooltip.findHoverIndexFromData(posX, series)).toBe(4); }); }); diff --git a/public/app/plugins/panel/graph/specs/threshold_manager.test.ts b/public/app/plugins/panel/graph/specs/threshold_manager.test.ts index 4a7a46fc6b0..ecbc382923e 100644 --- a/public/app/plugins/panel/graph/specs/threshold_manager.test.ts +++ b/public/app/plugins/panel/graph/specs/threshold_manager.test.ts @@ -5,7 +5,7 @@ import { ThresholdManager } from '../threshold_manager'; describe('ThresholdManager', function() { function plotOptionsScenario(desc, func) { describe(desc, function() { - var ctx: any = { + const ctx: any = { panel: { thresholds: [], }, @@ -17,9 +17,9 @@ describe('ThresholdManager', function() { ctx.setup = function(thresholds, data) { ctx.panel.thresholds = thresholds; - var manager = new ThresholdManager(ctx.panelCtrl); + const manager = new ThresholdManager(ctx.panelCtrl); if (data !== undefined) { - var element = angular.element('
    '); + const element = angular.element('
    '); manager.prepare(element, data); } manager.addFlotOptions(ctx.options, ctx.panel); @@ -34,7 +34,7 @@ describe('ThresholdManager', function() { ctx.setup([{ op: 'gt', value: 300, fill: true, line: true, colorMode: 'critical' }]); it('should add fill for threshold with fill: true', function() { - var markings = ctx.options.grid.markings; + const markings = ctx.options.grid.markings; expect(markings[0].yaxis.from).toBe(300); expect(markings[0].yaxis.to).toBe(Infinity); @@ -42,7 +42,7 @@ describe('ThresholdManager', function() { }); it('should add line', function() { - var markings = ctx.options.grid.markings; + const markings = ctx.options.grid.markings; expect(markings[1].yaxis.from).toBe(300); expect(markings[1].yaxis.to).toBe(300); expect(markings[1].color).toBe('rgba(237, 46, 24, 0.60)'); @@ -56,13 +56,13 @@ describe('ThresholdManager', function() { ]); it('should add fill for first thresholds to next threshold', function() { - var markings = ctx.options.grid.markings; + const markings = ctx.options.grid.markings; expect(markings[0].yaxis.from).toBe(200); expect(markings[0].yaxis.to).toBe(300); }); it('should add fill for last thresholds to infinity', function() { - var markings = ctx.options.grid.markings; + const markings = ctx.options.grid.markings; expect(markings[1].yaxis.from).toBe(300); expect(markings[1].yaxis.to).toBe(Infinity); }); @@ -75,13 +75,13 @@ describe('ThresholdManager', function() { ]); it('should add fill for first thresholds to next threshold', function() { - var markings = ctx.options.grid.markings; + const markings = ctx.options.grid.markings; expect(markings[0].yaxis.from).toBe(300); expect(markings[0].yaxis.to).toBe(200); }); it('should add fill for last thresholds to itself', function() { - var markings = ctx.options.grid.markings; + const markings = ctx.options.grid.markings; expect(markings[1].yaxis.from).toBe(200); expect(markings[1].yaxis.to).toBe(200); }); @@ -94,20 +94,20 @@ describe('ThresholdManager', function() { ]); it('should add fill for first thresholds to next threshold', function() { - var markings = ctx.options.grid.markings; + const markings = ctx.options.grid.markings; expect(markings[0].yaxis.from).toBe(300); expect(markings[0].yaxis.to).toBe(Infinity); }); it('should add fill for last thresholds to itself', function() { - var markings = ctx.options.grid.markings; + const markings = ctx.options.grid.markings; expect(markings[1].yaxis.from).toBe(200); expect(markings[1].yaxis.to).toBe(-Infinity); }); }); plotOptionsScenario('for threshold on two Y axes', ctx => { - var data = new Array(2); + const data = new Array(2); data[0] = new TimeSeries({ datapoints: [[0, 1], [300, 2]], alias: 'left', @@ -127,12 +127,12 @@ describe('ThresholdManager', function() { ); it('should add first threshold for left axis', function() { - var markings = ctx.options.grid.markings; + const markings = ctx.options.grid.markings; expect(markings[0].yaxis.from).toBe(100); }); it('should add second threshold for right axis', function() { - var markings = ctx.options.grid.markings; + const markings = ctx.options.grid.markings; expect(markings[1].y2axis.from).toBe(200); }); }); diff --git a/public/app/plugins/panel/heatmap/specs/heatmap_ctrl.test.ts b/public/app/plugins/panel/heatmap/specs/heatmap_ctrl.test.ts index d9d929a2697..8e1623c7d6f 100644 --- a/public/app/plugins/panel/heatmap/specs/heatmap_ctrl.test.ts +++ b/public/app/plugins/panel/heatmap/specs/heatmap_ctrl.test.ts @@ -25,7 +25,7 @@ describe('HeatmapCtrl', function() { describe('when time series are outside range', function() { beforeEach(function() { - var data = [ + const data = [ { target: 'test.cpu1', datapoints: [[45, 1234567890], [60, 1234567899]], @@ -43,14 +43,14 @@ describe('HeatmapCtrl', function() { describe('when time series are inside range', function() { beforeEach(function() { - var range = { + const range = { from: moment() .subtract(1, 'days') .valueOf(), to: moment().valueOf(), }; - var data = [ + const data = [ { target: 'test.cpu1', datapoints: [[45, range.from + 1000], [60, range.from + 10000]], @@ -68,7 +68,7 @@ describe('HeatmapCtrl', function() { describe('datapointsCount given 2 series', function() { beforeEach(function() { - var data = [{ target: 'test.cpu1', datapoints: [] }, { target: 'test.cpu2', datapoints: [] }]; + const data = [{ target: 'test.cpu1', datapoints: [] }, { target: 'test.cpu2', datapoints: [] }]; ctx.ctrl.onDataReceived(data); }); diff --git a/public/app/plugins/panel/singlestat/specs/singlestat_panel.test.ts b/public/app/plugins/panel/singlestat/specs/singlestat_panel.test.ts index 028200147f7..114cdf132e1 100644 --- a/public/app/plugins/panel/singlestat/specs/singlestat_panel.test.ts +++ b/public/app/plugins/panel/singlestat/specs/singlestat_panel.test.ts @@ -3,7 +3,7 @@ import { getColorForValue } from '../module'; describe('grafanaSingleStat', function() { describe('legacy thresholds', () => { describe('positive thresholds', () => { - var data: any = { + const data: any = { colorMap: ['green', 'yellow', 'red'], thresholds: [20, 50], }; @@ -39,7 +39,7 @@ describe('grafanaSingleStat', function() { }); describe('negative thresholds', () => { - var data: any = { + const data: any = { colorMap: ['green', 'yellow', 'red'], thresholds: [0, 20], }; @@ -58,7 +58,7 @@ describe('grafanaSingleStat', function() { }); describe('negative thresholds', () => { - var data: any = { + const data: any = { colorMap: ['green', 'yellow', 'red'], thresholds: [-27, 20], }; diff --git a/public/app/plugins/panel/table/specs/renderer.test.ts b/public/app/plugins/panel/table/specs/renderer.test.ts index 22957d1aa66..b66984ba223 100644 --- a/public/app/plugins/panel/table/specs/renderer.test.ts +++ b/public/app/plugins/panel/table/specs/renderer.test.ts @@ -4,7 +4,7 @@ import { TableRenderer } from '../renderer'; describe('when rendering table', () => { describe('given 13 columns', () => { - var table = new TableModel(); + const table = new TableModel(); table.columns = [ { text: 'Time' }, { text: 'Value' }, @@ -24,7 +24,7 @@ describe('when rendering table', () => { [1388556366666, 1230, 40, undefined, '', '', 'my.host.com', 'host1', ['value1', 'value2'], 1, 2, 1, 2], ]; - var panel = { + const panel = { pageSize: 10, styles: [ { @@ -163,11 +163,11 @@ describe('when rendering table', () => { ], }; - var sanitize = function(value) { + const sanitize = function(value) { return 'sanitized'; }; - var templateSrv = { + const templateSrv = { replace: function(value, scopedVars) { if (scopedVars) { // For testing variables replacement in link @@ -179,75 +179,75 @@ describe('when rendering table', () => { }, }; - var renderer = new TableRenderer(panel, table, 'utc', sanitize, templateSrv); + const renderer = new TableRenderer(panel, table, 'utc', sanitize, templateSrv); it('time column should be formated', () => { - var html = renderer.renderCell(0, 0, 1388556366666); + const html = renderer.renderCell(0, 0, 1388556366666); expect(html).toBe('
    '); }); it('undefined time column should be rendered as -', () => { - var html = renderer.renderCell(0, 0, undefined); + const html = renderer.renderCell(0, 0, undefined); expect(html).toBe(''); }); it('null time column should be rendered as -', () => { - var html = renderer.renderCell(0, 0, null); + const html = renderer.renderCell(0, 0, null); expect(html).toBe(''); }); it('number column with unit specified should ignore style unit', () => { - var html = renderer.renderCell(5, 0, 1230); + const html = renderer.renderCell(5, 0, 1230); expect(html).toBe(''); }); it('number column should be formated', () => { - var html = renderer.renderCell(1, 0, 1230); + const html = renderer.renderCell(1, 0, 1230); expect(html).toBe(''); }); it('number style should ignore string values', () => { - var html = renderer.renderCell(1, 0, 'asd'); + const html = renderer.renderCell(1, 0, 'asd'); expect(html).toBe(''); }); it('colored cell should have style', () => { - var html = renderer.renderCell(2, 0, 40); + const html = renderer.renderCell(2, 0, 40); expect(html).toBe(''); }); it('colored cell should have style', () => { - var html = renderer.renderCell(2, 0, 55); + const html = renderer.renderCell(2, 0, 55); expect(html).toBe(''); }); it('colored cell should have style', () => { - var html = renderer.renderCell(2, 0, 85); + const html = renderer.renderCell(2, 0, 85); expect(html).toBe(''); }); it('unformated undefined should be rendered as string', () => { - var html = renderer.renderCell(3, 0, 'value'); + const html = renderer.renderCell(3, 0, 'value'); expect(html).toBe(''); }); it('string style with escape html should return escaped html', () => { - var html = renderer.renderCell(4, 0, '&breaking
    the
    row'); + const html = renderer.renderCell(4, 0, '&breaking
    the
    row'); expect(html).toBe('
    '); }); it('undefined formater should return escaped html', () => { - var html = renderer.renderCell(3, 0, '&breaking
    the
    row'); + const html = renderer.renderCell(3, 0, '&breaking
    the
    row'); expect(html).toBe('
    '); }); it('undefined value should render as -', () => { - var html = renderer.renderCell(3, 0, undefined); + const html = renderer.renderCell(3, 0, undefined); expect(html).toBe(''); }); it('sanitized value should render as', () => { - var html = renderer.renderCell(6, 0, 'text link'); + const html = renderer.renderCell(6, 0, 'text link'); expect(html).toBe(''); }); @@ -264,8 +264,8 @@ describe('when rendering table', () => { }); it('link should render as', () => { - var html = renderer.renderCell(7, 0, 'host1'); - var expectedHtml = ` + const html = renderer.renderCell(7, 0, 'host1'); + const expectedHtml = ` '); }); it('numeric value should be mapped to text', () => { - var html = renderer.renderCell(9, 0, 1); + const html = renderer.renderCell(9, 0, 1); expect(html).toBe(''); }); it('string numeric value should be mapped to text', () => { - var html = renderer.renderCell(9, 0, '0'); + const html = renderer.renderCell(9, 0, '0'); expect(html).toBe(''); }); it('string value should be mapped to text', () => { - var html = renderer.renderCell(9, 0, 'HELLO WORLD'); + const html = renderer.renderCell(9, 0, 'HELLO WORLD'); expect(html).toBe(''); }); it('array column value should be mapped to text', () => { - var html = renderer.renderCell(9, 0, ['value1', 'value2']); + const html = renderer.renderCell(9, 0, ['value1', 'value2']); expect(html).toBe(''); }); it('value should be mapped to text (range)', () => { - var html = renderer.renderCell(10, 0, 2); + const html = renderer.renderCell(10, 0, 2); expect(html).toBe(''); }); it('value should be mapped to text (range)', () => { - var html = renderer.renderCell(10, 0, 5); + const html = renderer.renderCell(10, 0, 5); expect(html).toBe(''); }); it('array column value should not be mapped to text', () => { - var html = renderer.renderCell(10, 0, ['value1', 'value2']); + const html = renderer.renderCell(10, 0, ['value1', 'value2']); expect(html).toBe(''); }); it('value should be mapped to text and colored cell should have style', () => { - var html = renderer.renderCell(11, 0, 1); + const html = renderer.renderCell(11, 0, 1); expect(html).toBe(''); }); it('value should be mapped to text and colored cell should have style', () => { - var html = renderer.renderCell(11, 0, '1'); + const html = renderer.renderCell(11, 0, '1'); expect(html).toBe(''); }); it('value should be mapped to text and colored cell should have style', () => { - var html = renderer.renderCell(11, 0, 0); + const html = renderer.renderCell(11, 0, 0); expect(html).toBe(''); }); it('value should be mapped to text and colored cell should have style', () => { - var html = renderer.renderCell(11, 0, '0'); + const html = renderer.renderCell(11, 0, '0'); expect(html).toBe(''); }); it('value should be mapped to text and colored cell should have style', () => { - var html = renderer.renderCell(11, 0, '2.1'); + const html = renderer.renderCell(11, 0, '2.1'); expect(html).toBe(''); }); it('value should be mapped to text (range) and colored cell should have style', () => { - var html = renderer.renderCell(12, 0, 0); + const html = renderer.renderCell(12, 0, 0); expect(html).toBe(''); }); it('value should be mapped to text (range) and colored cell should have style', () => { - var html = renderer.renderCell(12, 0, 1); + const html = renderer.renderCell(12, 0, 1); expect(html).toBe(''); }); it('value should be mapped to text (range) and colored cell should have style', () => { - var html = renderer.renderCell(12, 0, 4); + const html = renderer.renderCell(12, 0, 4); expect(html).toBe(''); }); it('value should be mapped to text (range) and colored cell should have style', () => { - var html = renderer.renderCell(12, 0, '7.1'); + const html = renderer.renderCell(12, 0, '7.1'); expect(html).toBe(''); }); }); diff --git a/public/app/plugins/panel/table/specs/transformers.test.ts b/public/app/plugins/panel/table/specs/transformers.test.ts index eefe3f9bdc0..2425d98f26d 100644 --- a/public/app/plugins/panel/table/specs/transformers.test.ts +++ b/public/app/plugins/panel/table/specs/transformers.test.ts @@ -1,11 +1,11 @@ import { transformers, transformDataToTable } from '../transformers'; describe('when transforming time series table', () => { - var table; + let table; describe('given 2 time series', () => { - var time = new Date().getTime(); - var timeSeries = [ + const time = new Date().getTime(); + const timeSeries = [ { target: 'series1', datapoints: [[12.12, time], [14.44, time + 1]], @@ -17,7 +17,7 @@ describe('when transforming time series table', () => { ]; describe('timeseries_to_rows', () => { - var panel = { + const panel = { transform: 'timeseries_to_rows', sort: { col: 0, desc: true }, }; @@ -43,7 +43,7 @@ describe('when transforming time series table', () => { }); describe('timeseries_to_columns', () => { - var panel = { + const panel = { transform: 'timeseries_to_columns', }; @@ -70,7 +70,7 @@ describe('when transforming time series table', () => { }); describe('timeseries_aggregations', () => { - var panel = { + const panel = { transform: 'timeseries_aggregations', sort: { col: 0, desc: true }, columns: [{ text: 'Max', value: 'max' }, { text: 'Min', value: 'min' }], @@ -99,12 +99,12 @@ describe('when transforming time series table', () => { describe('table data sets', () => { describe('Table', () => { const transform = 'table'; - var panel = { + const panel = { transform, }; - var time = new Date().getTime(); + const time = new Date().getTime(); - var nonTableData = [ + const nonTableData = [ { type: 'foo', columns: [{ text: 'Time' }, { text: 'Label Key 1' }, { text: 'Value' }], @@ -112,7 +112,7 @@ describe('when transforming time series table', () => { }, ]; - var singleQueryData = [ + const singleQueryData = [ { type: 'table', columns: [{ text: 'Time' }, { text: 'Label Key 1' }, { text: 'Value' }], @@ -120,7 +120,7 @@ describe('when transforming time series table', () => { }, ]; - var multipleQueriesDataSameLabels = [ + const multipleQueriesDataSameLabels = [ { type: 'table', columns: [{ text: 'Time' }, { text: 'Label Key 1' }, { text: 'Label Key 2' }, { text: 'Value #A' }], @@ -143,7 +143,7 @@ describe('when transforming time series table', () => { }, ]; - var multipleQueriesDataDifferentLabels = [ + const multipleQueriesDataDifferentLabels = [ { type: 'table', columns: [{ text: 'Time' }, { text: 'Label Key 1' }, { text: 'Value #A' }], @@ -163,14 +163,14 @@ describe('when transforming time series table', () => { describe('getColumns', function() { it('should return data columns given a single query', function() { - var columns = transformers[transform].getColumns(singleQueryData); + const columns = transformers[transform].getColumns(singleQueryData); expect(columns[0].text).toBe('Time'); expect(columns[1].text).toBe('Label Key 1'); expect(columns[2].text).toBe('Value'); }); it('should return the union of data columns given a multiple queries', function() { - var columns = transformers[transform].getColumns(multipleQueriesDataSameLabels); + const columns = transformers[transform].getColumns(multipleQueriesDataSameLabels); expect(columns[0].text).toBe('Time'); expect(columns[1].text).toBe('Label Key 1'); expect(columns[2].text).toBe('Label Key 2'); @@ -179,7 +179,7 @@ describe('when transforming time series table', () => { }); it('should return the union of data columns given a multiple queries with different labels', function() { - var columns = transformers[transform].getColumns(multipleQueriesDataDifferentLabels); + const columns = transformers[transform].getColumns(multipleQueriesDataDifferentLabels); expect(columns[0].text).toBe('Time'); expect(columns[1].text).toBe('Label Key 1'); expect(columns[2].text).toBe('Value #A'); @@ -263,7 +263,7 @@ describe('when transforming time series table', () => { describe('doc data sets', () => { describe('JSON Data', () => { - var panel = { + const panel = { transform: 'json', columns: [ { text: 'Timestamp', value: 'timestamp' }, @@ -271,7 +271,7 @@ describe('when transforming time series table', () => { { text: 'nested.level2', value: 'nested.level2' }, ], }; - var rawData = [ + const rawData = [ { type: 'docs', datapoints: [ @@ -288,7 +288,7 @@ describe('when transforming time series table', () => { describe('getColumns', function() { it('should return nested properties', function() { - var columns = transformers['json'].getColumns(rawData); + const columns = transformers['json'].getColumns(rawData); expect(columns[0].text).toBe('timestamp'); expect(columns[1].text).toBe('message'); expect(columns[2].text).toBe('nested.level2'); @@ -319,8 +319,8 @@ describe('when transforming time series table', () => { describe('annotation data', () => { describe('Annnotations', () => { - var panel = { transform: 'annotations' }; - var rawData = { + const panel = { transform: 'annotations' }; + const rawData = { annotations: [ { time: 1000, diff --git a/public/test/jest-setup.ts b/public/test/jest-setup.ts index 1608f890315..fed65097ac7 100644 --- a/public/test/jest-setup.ts +++ b/public/test/jest-setup.ts @@ -18,5 +18,5 @@ jest.mock('app/features/plugins/plugin_loader', () => ({})); configure({ adapter: new Adapter() }); -var global = window; +const global = window; global.$ = global.jQuery = $; diff --git a/public/test/specs/helpers.ts b/public/test/specs/helpers.ts index 677419f3f75..960ce84f494 100644 --- a/public/test/specs/helpers.ts +++ b/public/test/specs/helpers.ts @@ -5,7 +5,7 @@ import { angularMocks, sinon } from '../lib/common'; import { PanelModel } from 'app/features/dashboard/panel_model'; export function ControllerTestContext() { - var self = this; + const self = this; this.datasource = {}; this.$element = {}; @@ -58,7 +58,7 @@ export function ControllerTestContext() { $rootScope.onAppEvent = sinon.spy(); $rootScope.colors = []; - for (var i = 0; i < 50; i++) { + for (let i = 0; i < 50; i++) { $rootScope.colors.push('#' + i); } @@ -88,7 +88,7 @@ export function ControllerTestContext() { self.scope.onAppEvent = sinon.spy(); $rootScope.colors = []; - for (var i = 0; i < 50; i++) { + for (let i = 0; i < 50; i++) { $rootScope.colors.push('#' + i); } @@ -107,7 +107,7 @@ export function ControllerTestContext() { } export function ServiceTestContext() { - var self = this; + const self = this; self.templateSrv = new TemplateSrvStub(); self.timeSrv = new TimeSrvStub(); self.datasourceSrv = {}; @@ -195,7 +195,7 @@ export function TemplateSrvStub() { }; } -var allDeps = { +const allDeps = { ContextSrvStub, TemplateSrvStub, TimeSrvStub, From 35c00891e722cc68dfb9cefe8537d5229661754c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Sun, 26 Aug 2018 20:19:23 +0200 Subject: [PATCH 509/786] tslint: more const fixes (#13035) --- public/app/core/config.ts | 6 ++-- .../features/admin/admin_edit_user_ctrl.ts | 6 ++-- .../alerting/notification_edit_ctrl.ts | 2 +- .../annotations/specs/annotations_srv.test.ts | 6 +--- .../app/features/dashboard/ad_hoc_filters.ts | 12 +++---- .../app/features/dashboard/change_tracker.ts | 18 +++++----- .../dashboard/dashboard_import_ctrl.ts | 12 +++---- .../dashboard/dashgrid/AddPanelPanel.tsx | 2 +- .../features/dashboard/export/export_modal.ts | 4 +-- .../app/features/dashboard/export/exporter.ts | 22 ++++++------ .../dashboard/repeat_option/repeat_option.ts | 2 +- .../features/dashboard/settings/settings.ts | 2 +- .../dashboard/specs/change_tracker.test.ts | 2 +- .../specs/dashboard_import_ctrl.test.ts | 2 +- .../specs/dashboard_migration.test.ts | 11 +++--- public/app/features/dashboard/time_srv.ts | 34 +++++++++---------- .../dashboard/timepicker/input_date.ts | 6 ++-- .../dashboard/timepicker/timepicker.ts | 10 +++--- public/app/features/dashboard/upload.ts | 10 +++--- public/app/features/org/org_api_keys_ctrl.ts | 2 +- public/app/features/org/org_details_ctrl.ts | 2 +- public/app/features/org/prefs_control.ts | 4 +-- public/app/features/panel/panel_directive.ts | 26 +++++++------- public/app/features/panel/panel_editor_tab.ts | 8 ++--- public/app/features/panel/panel_header.ts | 2 +- public/app/features/panel/query_editor_row.ts | 6 ++-- .../features/panel/query_troubleshooter.ts | 2 +- public/app/features/panel/solo_panel_ctrl.ts | 4 +-- .../features/playlist/playlist_edit_ctrl.ts | 8 ++--- .../app/features/playlist/playlist_search.ts | 4 +-- public/app/features/playlist/playlist_srv.ts | 4 +-- public/app/features/styleguide/styleguide.ts | 2 +- 32 files changed, 120 insertions(+), 123 deletions(-) diff --git a/public/app/core/config.ts b/public/app/core/config.ts index e065ddb22fb..f522c6340e6 100644 --- a/public/app/core/config.ts +++ b/public/app/core/config.ts @@ -31,7 +31,7 @@ export class Settings { loginError: any; constructor(options) { - var defaults = { + const defaults = { datasources: {}, window_title_prefix: 'Grafana - ', panels: {}, @@ -51,8 +51,8 @@ export class Settings { } } -var bootData = (window).grafanaBootData || { settings: {} }; -var options = bootData.settings; +const bootData = (window).grafanaBootData || { settings: {} }; +const options = bootData.settings; options.bootData = bootData; const config = new Settings(options); diff --git a/public/app/features/admin/admin_edit_user_ctrl.ts b/public/app/features/admin/admin_edit_user_ctrl.ts index 1d4fb9cf19a..b84b690d44a 100644 --- a/public/app/features/admin/admin_edit_user_ctrl.ts +++ b/public/app/features/admin/admin_edit_user_ctrl.ts @@ -29,14 +29,14 @@ export class AdminEditUserCtrl { return; } - var payload = { password: $scope.password }; + const payload = { password: $scope.password }; backendSrv.put('/api/admin/users/' + $scope.user_id + '/password', payload).then(function() { $location.path('/admin/users'); }); }; $scope.updatePermissions = function() { - var payload = $scope.permissions; + const payload = $scope.permissions; backendSrv.put('/api/admin/users/' + $scope.user_id + '/permissions', payload).then(function() { $location.path('/admin/users'); @@ -99,7 +99,7 @@ export class AdminEditUserCtrl { return; } - var orgInfo = _.find($scope.orgsSearchCache, { + const orgInfo = _.find($scope.orgsSearchCache, { name: $scope.newOrg.name, }); if (!orgInfo) { diff --git a/public/app/features/alerting/notification_edit_ctrl.ts b/public/app/features/alerting/notification_edit_ctrl.ts index eb14766d1fb..60942e6ffb4 100644 --- a/public/app/features/alerting/notification_edit_ctrl.ts +++ b/public/app/features/alerting/notification_edit_ctrl.ts @@ -99,7 +99,7 @@ export class AlertNotificationEditCtrl { return; } - var payload = { + const payload = { name: this.model.name, type: this.model.type, settings: this.model.settings, diff --git a/public/app/features/annotations/specs/annotations_srv.test.ts b/public/app/features/annotations/specs/annotations_srv.test.ts index 97696767536..f262544da43 100644 --- a/public/app/features/annotations/specs/annotations_srv.test.ts +++ b/public/app/features/annotations/specs/annotations_srv.test.ts @@ -6,12 +6,8 @@ describe('AnnotationsSrv', function() { const $rootScope = { onAppEvent: jest.fn(), }; - let $q; - let datasourceSrv; - let backendSrv; - let timeSrv; - const annotationsSrv = new AnnotationsSrv($rootScope, $q, datasourceSrv, backendSrv, timeSrv); + const annotationsSrv = new AnnotationsSrv($rootScope, null, null, null, null); describe('When translating the query result', () => { const annotationSource = { diff --git a/public/app/features/dashboard/ad_hoc_filters.ts b/public/app/features/dashboard/ad_hoc_filters.ts index 412761dc716..68b068152b5 100644 --- a/public/app/features/dashboard/ad_hoc_filters.ts +++ b/public/app/features/dashboard/ad_hoc_filters.ts @@ -55,8 +55,8 @@ export class AdHocFiltersCtrl { } return this.datasourceSrv.get(this.variable.datasource).then(ds => { - var options: any = {}; - var promise = null; + const options: any = {}; + let promise = null; if (segment.type !== 'value') { promise = ds.getTagKeys(); @@ -113,9 +113,9 @@ export class AdHocFiltersCtrl { } updateVariableModel() { - var filters = []; - var filterIndex = -1; - var hasFakes = false; + const filters = []; + let filterIndex = -1; + let hasFakes = false; this.segments.forEach(segment => { if (segment.type === 'value' && segment.fake) { @@ -153,7 +153,7 @@ export class AdHocFiltersCtrl { } } -var template = ` +const template = `
    { + const self = this; + const cancel = this.$rootScope.$on('dashboard-saved', () => { cancel(); this.$timeout(() => { self.gotoNext(); @@ -179,8 +179,8 @@ export class ChangeTracker { } gotoNext() { - var baseLen = this.$location.absUrl().length - this.$location.url().length; - var nextUrl = this.next.substring(baseLen); + const baseLen = this.$location.absUrl().length - this.$location.url().length; + const nextUrl = this.next.substring(baseLen); this.$location.url(nextUrl); } } diff --git a/public/app/features/dashboard/dashboard_import_ctrl.ts b/public/app/features/dashboard/dashboard_import_ctrl.ts index b70a1847602..3dfae1250dd 100644 --- a/public/app/features/dashboard/dashboard_import_ctrl.ts +++ b/public/app/features/dashboard/dashboard_import_ctrl.ts @@ -52,7 +52,7 @@ export class DashboardImportCtrl { if (this.dash.__inputs) { for (const input of this.dash.__inputs) { - var inputModel = { + const inputModel = { name: input.name, label: input.label, info: input.description, @@ -78,7 +78,7 @@ export class DashboardImportCtrl { } setDatasourceOptions(input, inputModel) { - var sources = _.filter(config.datasources, val => { + const sources = _.filter(config.datasources, val => { return val.type === input.pluginId; }); @@ -162,7 +162,7 @@ export class DashboardImportCtrl { } saveDashboard() { - var inputs = this.inputs.map(input => { + const inputs = this.inputs.map(input => { return { name: input.name, type: input.type, @@ -186,7 +186,7 @@ export class DashboardImportCtrl { loadJsonText() { try { this.parseError = ''; - var dash = JSON.parse(this.jsonText); + const dash = JSON.parse(this.jsonText); this.onUpload(dash); } catch (err) { console.log(err); @@ -198,8 +198,8 @@ export class DashboardImportCtrl { checkGnetDashboard() { this.gnetError = ''; - var match = /(^\d+$)|dashboards\/(\d+)/.exec(this.gnetUrl); - var dashboardId; + const match = /(^\d+$)|dashboards\/(\d+)/.exec(this.gnetUrl); + let dashboardId; if (match && match[1]) { dashboardId = match[1]; diff --git a/public/app/features/dashboard/dashgrid/AddPanelPanel.tsx b/public/app/features/dashboard/dashgrid/AddPanelPanel.tsx index 9459fc41753..a26a0401d56 100644 --- a/public/app/features/dashboard/dashgrid/AddPanelPanel.tsx +++ b/public/app/features/dashboard/dashgrid/AddPanelPanel.tsx @@ -97,7 +97,7 @@ export class AddPanelPanel extends React.Component { + const templateizeDatasourceUsage = obj => { // ignore data source properties that contain a variable if (obj.datasource && obj.datasource.indexOf('$') === 0) { if (variableLookup[obj.datasource.substring(1)]) { @@ -42,7 +42,7 @@ export class DashboardExporter { return; } - var refName = 'DS_' + ds.name.replace(' ', '_').toUpperCase(); + const refName = 'DS_' + ds.name.replace(' ', '_').toUpperCase(); datasources[refName] = { name: refName, label: ds.name, @@ -76,7 +76,7 @@ export class DashboardExporter { } } - var panelDef = config.panels[panel.type]; + const panelDef = config.panels[panel.type]; if (panelDef) { requires['panel' + panelDef.id] = { type: 'panel', @@ -131,7 +131,7 @@ export class DashboardExporter { // templatize constants for (const variable of saveModel.templating.list) { if (variable.type === 'constant') { - var refName = 'VAR_' + variable.name.replace(' ', '_').toUpperCase(); + const refName = 'VAR_' + variable.name.replace(' ', '_').toUpperCase(); inputs.push({ name: refName, type: 'constant', @@ -149,7 +149,7 @@ export class DashboardExporter { } // make inputs and requires a top thing - var newObj = {}; + const newObj = {}; newObj['__inputs'] = inputs; newObj['__requires'] = _.sortBy(requires, ['id']); diff --git a/public/app/features/dashboard/repeat_option/repeat_option.ts b/public/app/features/dashboard/repeat_option/repeat_option.ts index 696c634ddae..01e1d716fc5 100644 --- a/public/app/features/dashboard/repeat_option/repeat_option.ts +++ b/public/app/features/dashboard/repeat_option/repeat_option.ts @@ -1,6 +1,6 @@ import { coreModule } from 'app/core/core'; -var template = ` +const template = `
    2014-01-01T06:06:06Z--1.23 kbps1.230 sasd40.055.085.0value&breaking <br /> the <br /> row&breaking <br /> the <br /> rowsanitizedvalue1, value2onoffHELLO GRAFANAvalue3, value4onoffvalue1, value2ononoffoff2.10onoff7.1
    + + + + + + + + + + + + + + +
    + Name + + Start url +
    + {{playlist.name}} + + playlists/play/{{playlist.id}} + + + + Play + + + + + Edit + + + + + +
  • - - - - - - - - - - - - - - - -
    NameStart url
    - {{playlist.name}} - - playlists/play/{{playlist.id}} - - - - Play - - - - - Edit - - - - - -
    +
    + +
    From b6584f5ad0bc713b9686a3ed3bf3d09432667741 Mon Sep 17 00:00:00 2001 From: Peter Holmberg Date: Tue, 28 Aug 2018 15:23:25 +0200 Subject: [PATCH 522/786] Moved tooltip icon from input to label #12945 (#13059) --- .../plugins/panel/table/column_options.html | 43 +++++++++++-------- yarn.lock | 2 +- 2 files changed, 25 insertions(+), 20 deletions(-) diff --git a/public/app/plugins/panel/table/column_options.html b/public/app/plugins/panel/table/column_options.html index 4a4a6d0db9c..6f9adb4ae0f 100644 --- a/public/app/plugins/panel/table/column_options.html +++ b/public/app/plugins/panel/table/column_options.html @@ -156,30 +156,35 @@
    Link
    - + - -

    Specify an URL (relative or absolute)

    - - Use special variables to specify cell values: -
    - ${__cell} refers to current cell value -
    - ${__cell_n} refers to Nth column value in current row. Column indexes are started from 0. For instance, - ${__cell_1} refers to second column's value. -
    -
    - + - -

    Specify text for link tooltip.

    - - This title appears when user hovers pointer over the cell with link. Use the same variables as for URL. - -
    diff --git a/yarn.lock b/yarn.lock index fb593043288..c15c77cc45f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -850,7 +850,7 @@ async@^1.4.0, async@^1.5.0, async@^1.5.2, async@~1.5.2: version "1.5.2" resolved "https://registry.yarnpkg.com/async/-/async-1.5.2.tgz#ec6a61ae56480c0c3cb241c95618e20892f9672a" -async@^2.0.0, async@^2.1.4, async@^2.4.1, async@^2.6.0: +async@^2.0.0, async@^2.1.4, async@^2.6.0: version "2.6.1" resolved "https://registry.yarnpkg.com/async/-/async-2.6.1.tgz#b245a23ca71930044ec53fa46aa00a3e87c6a610" dependencies: From ff7b0d4f6347366bcc6827d49359fba568e81f63 Mon Sep 17 00:00:00 2001 From: bergquist Date: Tue, 28 Aug 2018 22:14:15 +0200 Subject: [PATCH 523/786] go fmt fixes --- pkg/models/datasource.go | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/pkg/models/datasource.go b/pkg/models/datasource.go index b7e3e3eaa17..cbdd0136f4d 100644 --- a/pkg/models/datasource.go +++ b/pkg/models/datasource.go @@ -59,22 +59,22 @@ type DataSource struct { } var knownDatasourcePlugins = map[string]bool{ - DS_ES: true, - DS_GRAPHITE: true, - DS_INFLUXDB: true, - DS_INFLUXDB_08: true, - DS_KAIROSDB: true, - DS_CLOUDWATCH: true, - DS_PROMETHEUS: true, - DS_OPENTSDB: true, - DS_POSTGRES: true, - DS_MYSQL: true, - DS_MSSQL: true, - "opennms": true, - "abhisant-druid-datasource": true, - "dalmatinerdb-datasource": true, - "gnocci": true, - "zabbix": true, + DS_ES: true, + DS_GRAPHITE: true, + DS_INFLUXDB: true, + DS_INFLUXDB_08: true, + DS_KAIROSDB: true, + DS_CLOUDWATCH: true, + DS_PROMETHEUS: true, + DS_OPENTSDB: true, + DS_POSTGRES: true, + DS_MYSQL: true, + DS_MSSQL: true, + "opennms": true, + "abhisant-druid-datasource": true, + "dalmatinerdb-datasource": true, + "gnocci": true, + "zabbix": true, "alexanderzobnin-zabbix-datasource": true, "newrelic-app": true, "grafana-datadog-datasource": true, From 12c98608826250ed481197fa1eeeb2aae2457c3d Mon Sep 17 00:00:00 2001 From: bergquist Date: Tue, 28 Aug 2018 22:26:47 +0200 Subject: [PATCH 524/786] string formating fixes --- pkg/api/live/conn.go | 2 +- pkg/cmd/grafana-cli/services/services.go | 4 ++-- pkg/components/imguploader/s3uploader.go | 2 +- pkg/log/log.go | 2 +- pkg/login/ext_user.go | 4 ++-- pkg/middleware/auth_proxy.go | 6 +++--- pkg/services/alerting/notifiers/telegram.go | 2 +- pkg/services/sqlstore/sqlstore.go | 2 +- pkg/services/sqlstore/transactions.go | 2 +- pkg/setting/setting.go | 2 +- 10 files changed, 14 insertions(+), 14 deletions(-) diff --git a/pkg/api/live/conn.go b/pkg/api/live/conn.go index f2a041d7631..0fae7f75b73 100644 --- a/pkg/api/live/conn.go +++ b/pkg/api/live/conn.go @@ -70,7 +70,7 @@ func (c *connection) readPump() { func (c *connection) handleMessage(message []byte) { json, err := simplejson.NewJson(message) if err != nil { - log.Error(3, "Unreadable message on websocket channel:", err) + log.Error(3, "Unreadable message on websocket channel. error: %v", err) } msgType := json.Get("action").MustString() diff --git a/pkg/cmd/grafana-cli/services/services.go b/pkg/cmd/grafana-cli/services/services.go index e743d42022c..b4e50ac84df 100644 --- a/pkg/cmd/grafana-cli/services/services.go +++ b/pkg/cmd/grafana-cli/services/services.go @@ -63,7 +63,7 @@ func ListAllPlugins(repoUrl string) (m.PluginRepo, error) { var data m.PluginRepo err = json.Unmarshal(body, &data) if err != nil { - logger.Info("Failed to unmarshal graphite response error: %v", err) + logger.Info("Failed to unmarshal graphite response error:", err) return m.PluginRepo{}, err } @@ -140,7 +140,7 @@ func GetPlugin(pluginId, repoUrl string) (m.Plugin, error) { var data m.Plugin err = json.Unmarshal(body, &data) if err != nil { - logger.Info("Failed to unmarshal graphite response error: %v", err) + logger.Info("Failed to unmarshal graphite response error:", err) return m.Plugin{}, err } diff --git a/pkg/components/imguploader/s3uploader.go b/pkg/components/imguploader/s3uploader.go index 62196357c61..a1e4aed0f47 100644 --- a/pkg/components/imguploader/s3uploader.go +++ b/pkg/components/imguploader/s3uploader.go @@ -60,7 +60,7 @@ func (u *S3Uploader) Upload(ctx context.Context, imageDiskPath string) (string, s3_endpoint, _ := endpoints.DefaultResolver().EndpointFor("s3", u.region) key := u.path + util.GetRandomString(20) + ".png" image_url := s3_endpoint.URL + "/" + u.bucket + "/" + key - log.Debug("Uploading image to s3", "url = ", image_url) + log.Debug("Uploading image to s3. url = %s", image_url) file, err := os.Open(imageDiskPath) if err != nil { diff --git a/pkg/log/log.go b/pkg/log/log.go index 0e6874e1b4b..8154b9b7f07 100644 --- a/pkg/log/log.go +++ b/pkg/log/log.go @@ -103,7 +103,7 @@ func Critical(skip int, format string, v ...interface{}) { } func Fatal(skip int, format string, v ...interface{}) { - Root.Crit(fmt.Sprintf(format, v)) + Root.Crit(fmt.Sprintf(format, v...)) Close() os.Exit(1) } diff --git a/pkg/login/ext_user.go b/pkg/login/ext_user.go index a421e3ebe0a..1262c1cc44f 100644 --- a/pkg/login/ext_user.go +++ b/pkg/login/ext_user.go @@ -35,7 +35,7 @@ func UpsertUser(cmd *m.UpsertUserCommand) error { limitReached, err := quota.QuotaReached(cmd.ReqContext, "user") if err != nil { - log.Warn("Error getting user quota", "err", err) + log.Warn("Error getting user quota. error: %v", err) return ErrGettingUserQuota } if limitReached { @@ -135,7 +135,7 @@ func updateUser(user *m.User, extUser *m.ExternalUserInfo) error { return nil } - log.Debug("Syncing user info", "id", user.Id, "update", updateCmd) + log.Debug2("Syncing user info", "id", user.Id, "update", updateCmd) return bus.Dispatch(updateCmd) } diff --git a/pkg/middleware/auth_proxy.go b/pkg/middleware/auth_proxy.go index 144a0ae3a69..29bd305b336 100644 --- a/pkg/middleware/auth_proxy.go +++ b/pkg/middleware/auth_proxy.go @@ -36,7 +36,7 @@ func initContextWithAuthProxy(ctx *m.ReqContext, orgID int64) bool { // initialize session if err := ctx.Session.Start(ctx.Context); err != nil { - log.Error(3, "Failed to start session", err) + log.Error(3, "Failed to start session. error %v", err) return false } @@ -146,12 +146,12 @@ func initContextWithAuthProxy(ctx *m.ReqContext, orgID int64) bool { if getRequestUserId(ctx) > 0 && getRequestUserId(ctx) != query.Result.UserId { // remove session if err := ctx.Session.Destory(ctx.Context); err != nil { - log.Error(3, "Failed to destroy session, err") + log.Error(3, "Failed to destroy session. error: %v", err) } // initialize a new session if err := ctx.Session.Start(ctx.Context); err != nil { - log.Error(3, "Failed to start session", err) + log.Error(3, "Failed to start session. error: %v", err) } } diff --git a/pkg/services/alerting/notifiers/telegram.go b/pkg/services/alerting/notifiers/telegram.go index ca24c996914..d79552079d5 100644 --- a/pkg/services/alerting/notifiers/telegram.go +++ b/pkg/services/alerting/notifiers/telegram.go @@ -216,7 +216,7 @@ func appendIfPossible(message string, extra string, sizeLimit int) string { if len(extra)+len(message) <= sizeLimit { return message + extra } - log.Debug("Line too long for image caption.", "value", extra) + log.Debug("Line too long for image caption. value: %s", extra) return message } diff --git a/pkg/services/sqlstore/sqlstore.go b/pkg/services/sqlstore/sqlstore.go index 13d706b6198..5477bc7b2d1 100644 --- a/pkg/services/sqlstore/sqlstore.go +++ b/pkg/services/sqlstore/sqlstore.go @@ -106,7 +106,7 @@ func (ss *SqlStore) inTransactionWithRetryCtx(ctx context.Context, callback dbTr if len(sess.events) > 0 { for _, e := range sess.events { if err = bus.Publish(e); err != nil { - log.Error(3, "Failed to publish event after commit", err) + log.Error(3, "Failed to publish event after commit. error: %v", err) } } } diff --git a/pkg/services/sqlstore/transactions.go b/pkg/services/sqlstore/transactions.go index eccd37f9a43..edf29fffb8f 100644 --- a/pkg/services/sqlstore/transactions.go +++ b/pkg/services/sqlstore/transactions.go @@ -89,7 +89,7 @@ func inTransactionWithRetryCtx(ctx context.Context, callback dbTransactionFunc, if len(sess.events) > 0 { for _, e := range sess.events { if err = bus.Publish(e); err != nil { - log.Error(3, "Failed to publish event after commit", err) + log.Error(3, "Failed to publish event after commit. error: %v", err) } } } diff --git a/pkg/setting/setting.go b/pkg/setting/setting.go index eb61568261d..aee9c00b526 100644 --- a/pkg/setting/setting.go +++ b/pkg/setting/setting.go @@ -324,7 +324,7 @@ func getCommandLineProperties(args []string) map[string]string { trimmed := strings.TrimPrefix(arg, "cfg:") parts := strings.Split(trimmed, "=") if len(parts) != 2 { - log.Fatal(3, "Invalid command line argument", arg) + log.Fatal(3, "Invalid command line argument. argument: %v", arg) return nil } From 84ec1ce624dbc3e6df4d09e59486ce386c37efe9 Mon Sep 17 00:00:00 2001 From: Sven Klemm Date: Wed, 29 Aug 2018 09:04:33 +0200 Subject: [PATCH 525/786] update filter macro on time column change --- .../plugins/datasource/postgres/query_ctrl.ts | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/public/app/plugins/datasource/postgres/query_ctrl.ts b/public/app/plugins/datasource/postgres/query_ctrl.ts index 6f2c1ce5d57..9a5b0dbe8ff 100644 --- a/public/app/plugins/datasource/postgres/query_ctrl.ts +++ b/public/app/plugins/datasource/postgres/query_ctrl.ts @@ -215,9 +215,25 @@ export class PostgresQueryCtrl extends QueryCtrl { this.target.timeColumn = this.timeColumnSegment.value; this.datasource.metricFindQuery(this.metaBuilder.buildDatatypeQuery(this.target.timeColumn)).then(result => { if (result.length === 1) { - this.target.timeColumnType = result[0].text; + if (this.target.timeColumnType !== result[0].text) { + this.target.timeColumnType = result[0].text; + let partModel; + if (this.queryModel.hasUnixEpochTimecolumn()) { + partModel = sqlPart.create({ type: 'macro', name: '$__unixEpochFilter', params: [] }); + } else { + partModel = sqlPart.create({ type: 'macro', name: '$__timeFilter', params: [] }); + } + + if (this.whereParts.length >= 1 && this.whereParts[0].def.type === 'macro') { + // replace current macro + this.whereParts[0] = partModel; + } else { + this.whereParts.splice(0, 0, partModel); + } + } } }); + this.updatePersistedParts(); this.panelCtrl.refresh(); } From 955e5afa459239d06c3f751edc2675bd88c79c76 Mon Sep 17 00:00:00 2001 From: Sven Klemm Date: Wed, 29 Aug 2018 09:49:23 +0200 Subject: [PATCH 526/786] recheck timecolumn when changing table --- .../app/plugins/datasource/postgres/query_ctrl.ts | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/public/app/plugins/datasource/postgres/query_ctrl.ts b/public/app/plugins/datasource/postgres/query_ctrl.ts index 9a5b0dbe8ff..43e9839e8bb 100644 --- a/public/app/plugins/datasource/postgres/query_ctrl.ts +++ b/public/app/plugins/datasource/postgres/query_ctrl.ts @@ -201,6 +201,19 @@ export class PostgresQueryCtrl extends QueryCtrl { tableChanged() { this.target.table = this.tableSegment.value; + this.datasource.metricFindQuery(this.metaBuilder.buildColumnQuery('time')).then(result => { + // check if time column is still valid + if (result.length > 0) { + if (!_.find(result, (r: any) => r.text === this.target.timeColumn)) { + let segment = this.uiSegmentSrv.newSegment(result[0].text); + this.timeColumnSegment.html = segment.html; + this.timeColumnSegment.value = segment.value; + + this.timeColumnChanged(); + } + } + }); + this.panelCtrl.refresh(); } From e9ab4feeb0531036a7bf15dd495f683df094ae24 Mon Sep 17 00:00:00 2001 From: Sven Klemm Date: Wed, 29 Aug 2018 09:58:08 +0200 Subject: [PATCH 527/786] link to github instead --- public/app/plugins/datasource/postgres/partials/config.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/app/plugins/datasource/postgres/partials/config.html b/public/app/plugins/datasource/postgres/partials/config.html index 6c0e5f04cad..f5c5c1dd5cc 100644 --- a/public/app/plugins/datasource/postgres/partials/config.html +++ b/public/app/plugins/datasource/postgres/partials/config.html @@ -76,7 +76,7 @@

    - TimescaleDB is a time-series database built as a PostgreSQL extension. If enabled, Grafana will use time_bucket in the $__timeGroup macro and display TimescaleDB specific aggregate functions in the query builder. + TimescaleDB is a time-series database built as a PostgreSQL extension. If enabled, Grafana will use time_bucket in the $__timeGroup macro and display TimescaleDB specific aggregate functions in the query builder.

    From ab4fbff454d4dccf0460dc55bbacd1baa52e3d63 Mon Sep 17 00:00:00 2001 From: Sven Klemm Date: Wed, 29 Aug 2018 12:12:11 +0200 Subject: [PATCH 528/786] handle quoting properly for table suggestion --- public/app/plugins/datasource/postgres/meta_query.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/public/app/plugins/datasource/postgres/meta_query.ts b/public/app/plugins/datasource/postgres/meta_query.ts index eefe011586d..a76e63a5438 100644 --- a/public/app/plugins/datasource/postgres/meta_query.ts +++ b/public/app/plugins/datasource/postgres/meta_query.ts @@ -27,9 +27,9 @@ export class PostgresMetaQuery { // query that returns first table found that has a timestamp(tz) column and a float column let query = ` SELECT - table_name, + quote_ident(table_name) as table_name, ( SELECT - column_name + quote_ident(column_name) as column_name FROM information_schema.columns c WHERE c.table_schema = t.table_schema AND @@ -38,7 +38,7 @@ SELECT ORDER BY ordinal_position LIMIT 1 ) AS time_column, ( SELECT - column_name + quote_ident(column_name) AS column_name FROM information_schema.columns c WHERE c.table_schema = t.table_schema AND From bfac6303d03632759735560e3a36998fbac20f64 Mon Sep 17 00:00:00 2001 From: Sven Klemm Date: Wed, 29 Aug 2018 12:14:17 +0200 Subject: [PATCH 529/786] strip quotes when auto adding alias --- public/app/plugins/datasource/postgres/query_ctrl.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/app/plugins/datasource/postgres/query_ctrl.ts b/public/app/plugins/datasource/postgres/query_ctrl.ts index 43e9839e8bb..211e246eae8 100644 --- a/public/app/plugins/datasource/postgres/query_ctrl.ts +++ b/public/app/plugins/datasource/postgres/query_ctrl.ts @@ -385,7 +385,7 @@ export class PostgresQueryCtrl extends QueryCtrl { if (addAlias) { // set initial alias name to column name - partModel = sqlPart.create({ type: 'alias', params: [selectParts[0].params[0]] }); + partModel = sqlPart.create({ type: 'alias', params: [selectParts[0].params[0].replace('"', '')] }); if (selectParts[selectParts.length - 1].def.type === 'alias') { selectParts[selectParts.length - 1] = partModel; } else { From 7a5b5906edad9a8ce98385fcdc03567a05dd0275 Mon Sep 17 00:00:00 2001 From: Sven Klemm Date: Wed, 29 Aug 2018 12:24:28 +0200 Subject: [PATCH 530/786] fix quoting --- public/app/plugins/datasource/postgres/postgres_query.ts | 8 ++++---- public/app/plugins/datasource/postgres/query_ctrl.ts | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/public/app/plugins/datasource/postgres/postgres_query.ts b/public/app/plugins/datasource/postgres/postgres_query.ts index 26d21e013fd..3e055f42f03 100644 --- a/public/app/plugins/datasource/postgres/postgres_query.ts +++ b/public/app/plugins/datasource/postgres/postgres_query.ts @@ -37,22 +37,22 @@ export default class PostgresQuery { // remove identifier quoting from identifier to use in metadata queries unquoteIdentifier(value) { if (value[0] === '"' && value[value.length - 1] === '"') { - return value.substring(1, value.length - 1).replace('""', '"'); + return value.substring(1, value.length - 1).replace(/""/g, '"'); } else { return value; } } quoteIdentifier(value) { - return '"' + value.replace('"', '""') + '"'; + return '"' + value.replace(/"/g, '""') + '"'; } quoteLiteral(value) { - return "'" + value.replace("'", "''") + "'"; + return "'" + value.replace(/'/g, "''") + "'"; } escapeLiteral(value) { - return value.replace("'", "''"); + return value.replace(/'/g, "''"); } hasTimeGroup() { diff --git a/public/app/plugins/datasource/postgres/query_ctrl.ts b/public/app/plugins/datasource/postgres/query_ctrl.ts index 211e246eae8..686907a7dc0 100644 --- a/public/app/plugins/datasource/postgres/query_ctrl.ts +++ b/public/app/plugins/datasource/postgres/query_ctrl.ts @@ -385,7 +385,7 @@ export class PostgresQueryCtrl extends QueryCtrl { if (addAlias) { // set initial alias name to column name - partModel = sqlPart.create({ type: 'alias', params: [selectParts[0].params[0].replace('"', '')] }); + partModel = sqlPart.create({ type: 'alias', params: [selectParts[0].params[0].replace(/"/g, '')] }); if (selectParts[selectParts.length - 1].def.type === 'alias') { selectParts[selectParts.length - 1] = partModel; } else { From 10f55f55117fc8c39270ecd2642b7472121c90fd Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Wed, 29 Aug 2018 12:34:27 +0200 Subject: [PATCH 531/786] changelog: add notes about 4.6.4 and 5.2.3 releases --- CHANGELOG.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5137e716b49..aed25afb02e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -73,6 +73,12 @@ These are new features that's still being worked on and are in an experimental p * **Frontend**: Convert all Frontend Karma tests to Jest tests [#12224](https://github.com/grafana/grafana/issues/12224) +# 5.2.3 (2018-08-29) + +### Important fix for LDAP & OAuth login vulnerability + +See [security announcement](https://community.grafana.com/t/grafana-5-2-3-and-4-6-4-security-update/10050) for details. + # 5.2.2 (2018-07-25) ### Minor @@ -441,6 +447,12 @@ The following properties have been deprecated and will be removed in a future re - `uri` property in `GET /api/search` -> Use new `url` or `uid` property instead - `meta.slug` property in `GET /api/dashboards/uid/:uid` and `GET /api/dashboards/db/:slug` -> Use new `meta.url` or `dashboard.uid` property instead +# 4.6.4 (2018-08-29) + +### Important fix for LDAP & OAuth login vulnerability + +See [security announcement](https://community.grafana.com/t/grafana-5-2-3-and-4-6-4-security-update/10050) for details. + # 4.6.3 (2017-12-14) ## Fixes From 0b74ff5cf17db92d7d4246688a9fde142f4fe6e9 Mon Sep 17 00:00:00 2001 From: Sven Klemm Date: Wed, 29 Aug 2018 12:51:58 +0200 Subject: [PATCH 532/786] remove unneeded queryOptions --- public/app/plugins/datasource/postgres/plugin.json | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/public/app/plugins/datasource/postgres/plugin.json b/public/app/plugins/datasource/postgres/plugin.json index f236aa01b06..2c2e1690a65 100644 --- a/public/app/plugins/datasource/postgres/plugin.json +++ b/public/app/plugins/datasource/postgres/plugin.json @@ -18,10 +18,6 @@ "alerting": true, "annotations": true, - "metrics": true, - - "queryOptions": { - "minInterval": true - } + "metrics": true } From 1ee91a637fb6cba317ecbd948046d3a45f224d08 Mon Sep 17 00:00:00 2001 From: Sven Klemm Date: Wed, 29 Aug 2018 13:02:25 +0200 Subject: [PATCH 533/786] remove min time interval from datasource config --- .../plugins/datasource/postgres/partials/config.html | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/public/app/plugins/datasource/postgres/partials/config.html b/public/app/plugins/datasource/postgres/partials/config.html index f5c5c1dd5cc..a4df858db7e 100644 --- a/public/app/plugins/datasource/postgres/partials/config.html +++ b/public/app/plugins/datasource/postgres/partials/config.html @@ -38,18 +38,6 @@ -
    -
    -
    - Min time interval - - - A lower limit for the auto group by time interval. Recommended to be set to write frequency, - for example 1m if your data is written every minute. - -
    -
    -

    PostgreSQL details

    From 1e2fde238c0e86bcc2cbb8041a20f01b5736d780 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Wed, 29 Aug 2018 13:26:23 +0200 Subject: [PATCH 534/786] docs: corrected docs description for setting --- docs/sources/installation/configuration.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/sources/installation/configuration.md b/docs/sources/installation/configuration.md index 4b14829b689..3394dfe16bc 100644 --- a/docs/sources/installation/configuration.md +++ b/docs/sources/installation/configuration.md @@ -266,7 +266,8 @@ The number of days the keep me logged in / remember me cookie lasts. ### secret_key -Used for signing keep me logged in / remember me cookies. +Used for signing some datasource settings like secrets and passwords. Cannot be changed without requiring an update +to datasource settings to re-encode them. ### disable_gravatar From 800ba84f671d0ef646a4bb1d2c2715da243db543 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Wed, 29 Aug 2018 13:29:29 +0200 Subject: [PATCH 535/786] update latest.json to latest stable version --- latest.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/latest.json b/latest.json index 8e26289c856..7b36131fea2 100644 --- a/latest.json +++ b/latest.json @@ -1,4 +1,4 @@ { - "stable": "5.2.0", - "testing": "5.2.0" + "stable": "5.2.3", + "testing": "5.2.3" } From 5e0d0c5816677a99f5408722ce46b83ca1f219e6 Mon Sep 17 00:00:00 2001 From: Patrick O'Carroll Date: Wed, 29 Aug 2018 14:26:50 +0200 Subject: [PATCH 536/786] changed var to const (#13061) --- public/app/core/app_events.ts | 2 +- .../components/code_editor/code_editor.ts | 4 +- .../app/core/components/dashboard_selector.ts | 2 +- .../core/components/json_explorer/helpers.ts | 4 +- .../components/json_explorer/json_explorer.ts | 2 +- .../app/core/components/jsontree/jsontree.ts | 2 +- .../layout_selector/layout_selector.ts | 4 +- .../core/components/query_part/query_part.ts | 6 +-- .../query_part/query_part_editor.ts | 38 +++++++------- public/app/core/components/search/search.ts | 4 +- .../app/core/components/sidemenu/sidemenu.ts | 4 +- public/app/core/components/switch.ts | 2 +- public/app/core/directives/give_focus.ts | 4 +- public/app/core/directives/misc.ts | 28 +++++------ .../app/core/directives/ng_model_on_blur.ts | 2 +- .../app/core/directives/rebuild_on_change.ts | 2 +- public/app/core/directives/tags.ts | 6 +-- public/app/core/jquery_extended.ts | 11 ++-- public/app/core/live/live_srv.ts | 6 +-- public/app/core/nav_model_srv.ts | 4 +- public/app/core/profiler.ts | 10 ++-- public/app/core/services/context_srv.ts | 2 +- public/app/core/services/impression_srv.ts | 2 +- public/app/core/services/ng_react.ts | 50 +++++++++---------- public/app/core/services/search_srv.ts | 2 +- public/app/core/utils/datemath.ts | 12 ++--- public/app/core/utils/emitter.ts | 2 +- public/app/core/utils/model_utils.ts | 2 +- public/app/core/utils/outline.ts | 8 +-- public/app/core/utils/sort_by_keys.ts | 2 +- public/app/core/utils/ticks.ts | 24 ++++----- public/app/features/alerting/alert_def.ts | 18 +++---- .../app/features/alerting/alert_tab_ctrl.ts | 30 +++++------ .../app/features/alerting/threshold_mapper.ts | 8 +-- .../features/dashboard/dashboard_migration.ts | 16 +++--- .../app/features/dashboard/dashboard_model.ts | 14 +++--- public/app/features/dashboard/panel_model.ts | 4 +- .../app/features/panel/metrics_panel_ctrl.ts | 22 ++++---- public/app/features/panel/metrics_tab.ts | 2 +- public/app/features/panel/panel_ctrl.ts | 28 +++++------ public/app/features/templating/variable.ts | 4 +- .../cloudwatch/query_parameter_ctrl.ts | 20 ++++---- .../datasource/graphite/add_graphite_func.ts | 14 +++--- .../datasource/graphite/func_editor.ts | 44 ++++++++-------- .../app/plugins/datasource/graphite/gfunc.ts | 16 +++--- .../datasource/graphite/graphite_query.ts | 20 ++++---- .../app/plugins/datasource/graphite/lexer.ts | 40 +++++++-------- .../app/plugins/datasource/graphite/parser.ts | 26 +++++----- .../plugins/datasource/graphite/query_ctrl.ts | 10 ++-- .../plugins/datasource/mixed/datasource.ts | 8 +-- 50 files changed, 298 insertions(+), 299 deletions(-) diff --git a/public/app/core/app_events.ts b/public/app/core/app_events.ts index 26dd74bcb00..6af7913167b 100644 --- a/public/app/core/app_events.ts +++ b/public/app/core/app_events.ts @@ -1,4 +1,4 @@ import { Emitter } from './utils/emitter'; -var appEvents = new Emitter(); +const appEvents = new Emitter(); export default appEvents; diff --git a/public/app/core/components/code_editor/code_editor.ts b/public/app/core/components/code_editor/code_editor.ts index 66aec778d73..6ae1a99f245 100644 --- a/public/app/core/components/code_editor/code_editor.ts +++ b/public/app/core/components/code_editor/code_editor.ts @@ -99,9 +99,9 @@ function link(scope, elem, attrs) { if (scope.codeEditorFocus) { setTimeout(function() { textarea.focus(); - var domEl = textarea[0]; + const domEl = textarea[0]; if (domEl.setSelectionRange) { - var pos = textarea.val().length * 2; + const pos = textarea.val().length * 2; domEl.setSelectionRange(pos, pos); } }, 100); diff --git a/public/app/core/components/dashboard_selector.ts b/public/app/core/components/dashboard_selector.ts index 379fd441a19..e1809f3d42c 100644 --- a/public/app/core/components/dashboard_selector.ts +++ b/public/app/core/components/dashboard_selector.ts @@ -1,6 +1,6 @@ import coreModule from 'app/core/core_module'; -var template = ` +const template = ` `; diff --git a/public/app/core/components/json_explorer/helpers.ts b/public/app/core/components/json_explorer/helpers.ts index c445e1b0667..bc7468b3b21 100644 --- a/public/app/core/components/json_explorer/helpers.ts +++ b/public/app/core/components/json_explorer/helpers.ts @@ -12,7 +12,7 @@ function escapeString(str: string): string { * Determines if a value is an object */ export function isObject(value: any): boolean { - var type = typeof value; + const type = typeof value; return !!value && type === 'object'; } @@ -55,7 +55,7 @@ export function getType(object: Object): string { * Generates inline preview for a JavaScript object based on a value */ export function getValuePreview(object: Object, value: string): string { - var type = getType(object); + const type = getType(object); if (type === 'null' || type === 'undefined') { return type; diff --git a/public/app/core/components/json_explorer/json_explorer.ts b/public/app/core/components/json_explorer/json_explorer.ts index 790ed442d5c..779e5a93cba 100644 --- a/public/app/core/components/json_explorer/json_explorer.ts +++ b/public/app/core/components/json_explorer/json_explorer.ts @@ -279,7 +279,7 @@ export class JsonExplorer { const objectWrapperSpan = createElement('span'); // get constructor name and append it to wrapper span - var constructorName = createElement('span', 'constructor-name', this.constructorName); + const constructorName = createElement('span', 'constructor-name', this.constructorName); objectWrapperSpan.appendChild(constructorName); // if it's an array append the array specific elements like brackets and length diff --git a/public/app/core/components/jsontree/jsontree.ts b/public/app/core/components/jsontree/jsontree.ts index e127d7b14a9..5fbda5560b3 100644 --- a/public/app/core/components/jsontree/jsontree.ts +++ b/public/app/core/components/jsontree/jsontree.ts @@ -11,7 +11,7 @@ coreModule.directive('jsonTree', [ rootName: '@', }, link: function(scope, elem) { - var jsonExp = new JsonExplorer(scope.object, 3, { + const jsonExp = new JsonExplorer(scope.object, 3, { animateOpen: true, }); diff --git a/public/app/core/components/layout_selector/layout_selector.ts b/public/app/core/components/layout_selector/layout_selector.ts index 91a3afea250..a28abe19251 100644 --- a/public/app/core/components/layout_selector/layout_selector.ts +++ b/public/app/core/components/layout_selector/layout_selector.ts @@ -1,7 +1,7 @@ import store from 'app/core/store'; import coreModule from 'app/core/core_module'; -var template = ` +const template = `
    - - @@ -42,6 +42,12 @@
    + +
    {stat.name}{stat.value}
    + // + // + // + // + // + // + // {serverStats.stats.map(StatItem)} + //
    NameValue
    + //
    + //
    + // ); + } +} + +function StatItem(stat) { + return ( + + {stat.name} + {stat.value} + + ); +} + +const mapStateToProps = state => ({ + navModel: state.navModel, +}); + +const mapDispatchToProps = { + initNav, +}; + +export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(ServerStats)); diff --git a/public/app/containers/ServerStats/__snapshots__/ServerStats.test.tsx.snap b/public/app/features/server-stats/__snapshots__/ServerStats.test.tsx.snap similarity index 100% rename from public/app/containers/ServerStats/__snapshots__/ServerStats.test.tsx.snap rename to public/app/features/server-stats/__snapshots__/ServerStats.test.tsx.snap diff --git a/public/app/routes/ReactContainer.tsx b/public/app/routes/ReactContainer.tsx index b161a5e7a87..3ed534da587 100644 --- a/public/app/routes/ReactContainer.tsx +++ b/public/app/routes/ReactContainer.tsx @@ -1,18 +1,22 @@ import React from 'react'; import ReactDOM from 'react-dom'; import { Provider } from 'mobx-react'; +import { Provider as ReduxProvider } from 'react-redux'; import coreModule from 'app/core/core_module'; import { store } from 'app/stores/store'; +import { store as reduxStore } from 'app/stores/configureStore'; import { BackendSrv } from 'app/core/services/backend_srv'; import { DatasourceSrv } from 'app/features/plugins/datasource_srv'; import { ContextSrv } from 'app/core/services/context_srv'; function WrapInProvider(store, Component, props) { return ( - - - + + + + + ); } diff --git a/public/app/routes/routes.ts b/public/app/routes/routes.ts index d12711aca5b..7fcab26645f 100644 --- a/public/app/routes/routes.ts +++ b/public/app/routes/routes.ts @@ -1,7 +1,7 @@ import './dashboard_loaders'; import './ReactContainer'; -import ServerStats from 'app/containers/ServerStats/ServerStats'; +import ServerStats from 'app/features/server-stats/ServerStats'; import AlertRuleList from 'app/containers/AlertRuleList/AlertRuleList'; import FolderSettings from 'app/containers/ManageDashboards/FolderSettings'; import FolderPermissions from 'app/containers/ManageDashboards/FolderPermissions'; diff --git a/public/app/store/nav/actions.ts b/public/app/store/nav/actions.ts deleted file mode 100644 index eca99cc2b90..00000000000 --- a/public/app/store/nav/actions.ts +++ /dev/null @@ -1,30 +0,0 @@ -// -// Only test actions to test redux & typescript -// - -export enum ActionTypes { - SET_NAV = 'SET_NAV', - SET_QUERY = 'SET_QUERY', -} - -export interface SetNavAction { - type: ActionTypes.SET_NAV; - payload: { - path: string; - query: object; - }; -} - -export interface SetQueryAction { - type: ActionTypes.SET_QUERY; - payload: { - query: object; - }; -} - -export type Action = SetNavAction | SetQueryAction; - -export const setNav = (path: string, query: object): SetNavAction => ({ - type: ActionTypes.SET_NAV, - payload: { path: path, query: query }, -}); diff --git a/public/app/store/nav/reducers.ts b/public/app/store/nav/reducers.ts deleted file mode 100644 index 6e9d6e713a0..00000000000 --- a/public/app/store/nav/reducers.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { Action, ActionTypes } from './actions'; - -export interface NavState { - path: string; - query: object; -} - -const initialState: NavState = { - path: '/test', - query: {}, -}; - -export const navReducer = (state: NavState = initialState, action: Action): NavState => { - switch (action.type) { - case ActionTypes.SET_NAV: { - return { ...state, path: action.payload.path, query: action.payload.query }; - } - - case ActionTypes.SET_QUERY: { - return { - ...state, - query: action.payload.query, - }; - } - - default: { - return state; - } - } -}; diff --git a/public/app/store/configureStore.ts b/public/app/stores/configureStore.ts similarity index 86% rename from public/app/store/configureStore.ts rename to public/app/stores/configureStore.ts index a0dfe576ed6..3a7d16da76d 100644 --- a/public/app/store/configureStore.ts +++ b/public/app/stores/configureStore.ts @@ -1,10 +1,10 @@ import { createStore, applyMiddleware, compose, combineReducers } from 'redux'; import thunk from 'redux-thunk'; import { createLogger } from 'redux-logger'; -import { navReducer } from './nav/reducers'; +import sharedReducers from 'app/core/reducers'; const rootReducer = combineReducers({ - nav: navReducer, + ...sharedReducers }); export let store; diff --git a/public/app/types/container.ts b/public/app/types/container.ts new file mode 100644 index 00000000000..174bc0c8460 --- /dev/null +++ b/public/app/types/container.ts @@ -0,0 +1,6 @@ +import { NavModel } from './navModel'; + +export interface ContainerProps { + navModel: NavModel; + initNav: (...args: string[]) => void; +} diff --git a/public/app/types/index.ts b/public/app/types/index.ts new file mode 100644 index 00000000000..43d921e3964 --- /dev/null +++ b/public/app/types/index.ts @@ -0,0 +1,4 @@ +import { NavModel, NavModelItem } from './navModel'; +import { ContainerProps } from './container'; + +export { NavModel, NavModelItem, ContainerProps }; diff --git a/public/app/types/navModel.ts b/public/app/types/navModel.ts new file mode 100644 index 00000000000..e1a4265847c --- /dev/null +++ b/public/app/types/navModel.ts @@ -0,0 +1,19 @@ +export interface NavModelItem { + text: string; + url: string; + subTitle?: string; + icon?: string; + img?: string; + id: string; + active?: boolean; + hideFromTabs?: boolean; + divider?: boolean; + children?: NavModelItem[]; + target?: string; +} + +export interface NavModel { + breadcrumbs: NavModelItem[]; + main: NavModelItem; + node: NavModelItem; +} From d68007fde37665ff847645bcf22191c5ec7c4fa9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Fri, 31 Aug 2018 14:38:23 +0200 Subject: [PATCH 573/786] wip: redux --- .../app/features/server-stats/ServerStats.tsx | 65 +++++++++++-------- public/app/features/server-stats/api.ts | 26 ++++++++ 2 files changed, 63 insertions(+), 28 deletions(-) create mode 100644 public/app/features/server-stats/api.ts diff --git a/public/app/features/server-stats/ServerStats.tsx b/public/app/features/server-stats/ServerStats.tsx index b499fb725a8..da1fb6e76f7 100644 --- a/public/app/features/server-stats/ServerStats.tsx +++ b/public/app/features/server-stats/ServerStats.tsx @@ -3,53 +3,61 @@ import { hot } from 'react-hot-loader'; import { connect } from 'react-redux'; import { initNav } from 'app/core/actions'; import { ContainerProps } from 'app/types'; +import { getServerStats, ServerStat } from './api'; import PageHeader from 'app/core/components/PageHeader/PageHeader'; -interface Props extends ContainerProps {} +interface Props extends ContainerProps { + getServerStats: () => Promise; +} -export class ServerStats extends React.Component { +interface State { + stats: ServerStat[]; +} + +export class ServerStats extends React.Component { constructor(props) { super(props); + this.state = { + stats: [], + }; + this.props.initNav('cfg', 'admin', 'server-stats'); - // const { nav, serverStats } = this.props; - // - // nav.load('cfg', 'admin', 'server-stats'); - // serverStats.load(); - // - // store.dispatch(setNav('new', { asd: 'tasd' })); + } + + async componentDidMount() { + try { + const stats = await this.props.getServerStats(); + this.setState({ stats }); + } catch (error) { + console.error(error); + } } render() { const { navModel } = this.props; - console.log('render', navModel); + const { stats } = this.state; + return (
    -

    aasd

    +
    + + + + + + + + {stats.map(StatItem)} +
    NameValue
    +
    ); - // const { nav, serverStats } = this.props; - // return ( - //
    - // - //
    - // - // - // - // - // - // - // - // {serverStats.stats.map(StatItem)} - //
    NameValue
    - //
    - //
    - // ); } } -function StatItem(stat) { +function StatItem(stat: ServerStat) { return ( {stat.name} @@ -60,6 +68,7 @@ function StatItem(stat) { const mapStateToProps = state => ({ navModel: state.navModel, + getServerStats: getServerStats, }); const mapDispatchToProps = { diff --git a/public/app/features/server-stats/api.ts b/public/app/features/server-stats/api.ts new file mode 100644 index 00000000000..888cfd4f58f --- /dev/null +++ b/public/app/features/server-stats/api.ts @@ -0,0 +1,26 @@ +import { getBackendSrv } from 'app/core/services/backend_srv'; + +export interface ServerStat { + name: string; + value: string; +} + +export const getServerStats = async (): Promise => { + try { + const res = await getBackendSrv().get('api/admin/stats'); + return [ + { name: 'Total users', value: res.users }, + { name: 'Total dashboards', value: res.dashboards }, + { name: 'Active users (seen last 30 days)', value: res.activeUsers }, + { name: 'Total orgs', value: res.orgs }, + { name: 'Total playlists', value: res.playlists }, + { name: 'Total snapshots', value: res.snapshots }, + { name: 'Total dashboard tags', value: res.tags }, + { name: 'Total starred dashboards', value: res.stars }, + { name: 'Total alerts', value: res.alerts }, + ]; + } catch (error) { + console.error(error); + throw error; + } +}; From 2996c54b72541e0b6c4081c3d5e760dce401c72f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Fri, 31 Aug 2018 07:37:21 -0700 Subject: [PATCH 574/786] fix: for text flickering in animation on chrome on windows --- public/views/index.template.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/views/index.template.html b/public/views/index.template.html index 5e3df7df80a..0a6af599280 100644 --- a/public/views/index.template.html +++ b/public/views/index.template.html @@ -78,7 +78,7 @@ margin-top: 16px; font-weight: 500; font-size: 14px; - font-family: 'Roboto'; + font-family: Sans-serif; opacity: 0; animation-name: preloader-fade-in; animation-duration: .9s; From 944c1da27b32cd4a39f9a34f31aaa5e2fc1dd27a Mon Sep 17 00:00:00 2001 From: Patrick O'Carroll Date: Fri, 31 Aug 2018 16:40:23 +0200 Subject: [PATCH 575/786] set member-access and no-var-keyword to true, removed public in two files (#13104) --- .../core/components/manage_dashboards/manage_dashboards.ts | 2 +- public/app/features/playlist/playlist_srv.ts | 2 +- tslint.json | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/public/app/core/components/manage_dashboards/manage_dashboards.ts b/public/app/core/components/manage_dashboards/manage_dashboards.ts index 0016305e617..da3fa2f8ab8 100644 --- a/public/app/core/components/manage_dashboards/manage_dashboards.ts +++ b/public/app/core/components/manage_dashboards/manage_dashboards.ts @@ -14,7 +14,7 @@ class Query { } export class ManageDashboardsCtrl { - public sections: any[]; + sections: any[]; query: Query; navModel: any; diff --git a/public/app/features/playlist/playlist_srv.ts b/public/app/features/playlist/playlist_srv.ts index 395f65d9b14..9d3b635a1e5 100644 --- a/public/app/features/playlist/playlist_srv.ts +++ b/public/app/features/playlist/playlist_srv.ts @@ -10,7 +10,7 @@ class PlaylistSrv { private index: number; private interval: any; private startUrl: string; - public isPlaying: boolean; + isPlaying: boolean; /** @ngInject */ constructor(private $location: any, private $timeout: any, private backendSrv: any) {} diff --git a/tslint.json b/tslint.json index 27ebab036d4..e9caf1d7b38 100644 --- a/tslint.json +++ b/tslint.json @@ -27,7 +27,7 @@ "indent": [true, "spaces", 2], "label-position": true, "max-line-length": [true, 150], - "member-access": false, + "member-access": [true, "no-public"], "no-arg": true, "no-bitwise": false, "no-console": [true, "debug", "info", "time", "timeEnd", "trace"], @@ -40,7 +40,7 @@ "no-string-literal": false, "no-switch-case-fall-through": false, "no-trailing-whitespace": true, - "no-var-keyword": false, + "no-var-keyword": true, "object-literal-sort-keys": false, "one-line": [true, "check-open-brace", "check-catch", "check-else"], "prefer-const": true, From abbb6f933c3e2eca02ca7b2b5b39a36cd437316a Mon Sep 17 00:00:00 2001 From: Patrick O'Carroll Date: Fri, 31 Aug 2018 16:40:43 +0200 Subject: [PATCH 576/786] added jsdoc-format rule and fixed files that didn't follow new rule (#13107) --- public/app/core/components/form_dropdown/form_dropdown.ts | 2 +- .../app/core/components/layout_selector/layout_selector.ts | 6 +++--- public/app/core/directives/rebuild_on_change.ts | 2 +- public/app/core/services/popover_srv.ts | 2 +- public/app/features/admin/admin.ts | 4 ++-- public/app/features/annotations/annotation_tooltip.ts | 2 +- public/app/features/annotations/event_editor.ts | 2 +- public/app/features/dashboard/create_folder_ctrl.ts | 2 +- .../app/features/dashboard/repeat_option/repeat_option.ts | 2 +- public/app/features/dashboard/share_snapshot_ctrl.ts | 2 +- public/app/features/dashboard/time_srv.ts | 2 +- public/app/features/org/change_password_ctrl.ts | 2 +- public/app/features/org/create_team_ctrl.ts | 2 +- public/app/features/org/new_org_ctrl.ts | 2 +- public/app/features/org/org_api_keys_ctrl.ts | 2 +- public/app/features/org/org_details_ctrl.ts | 2 +- public/app/features/org/prefs_control.ts | 2 +- public/app/features/org/profile_ctrl.ts | 2 +- public/app/features/org/select_org_ctrl.ts | 2 +- public/app/features/org/user_invite_ctrl.ts | 2 +- public/app/features/panel/metrics_tab.ts | 2 +- public/app/features/panel/panel_header.ts | 2 +- public/app/features/panel/query_editor_row.ts | 2 +- public/app/features/panel/query_troubleshooter.ts | 2 +- public/app/features/plugins/plugin_component.ts | 2 +- public/app/features/styleguide/styleguide.ts | 2 +- public/app/features/templating/adhoc_variable.ts | 2 +- public/app/features/templating/constant_variable.ts | 2 +- public/app/features/templating/custom_variable.ts | 2 +- public/app/features/templating/datasource_variable.ts | 2 +- public/app/features/templating/editor_ctrl.ts | 2 +- public/app/features/templating/interval_variable.ts | 2 +- public/app/features/templating/query_variable.ts | 2 +- public/app/plugins/datasource/cloudwatch/query_ctrl.ts | 2 +- public/app/plugins/datasource/elasticsearch/query_ctrl.ts | 2 +- public/app/plugins/datasource/graphite/query_ctrl.ts | 2 +- public/app/plugins/datasource/influxdb/query_ctrl.ts | 2 +- public/app/plugins/datasource/mssql/datasource.ts | 2 +- public/app/plugins/datasource/mssql/module.ts | 2 +- public/app/plugins/datasource/mssql/query_ctrl.ts | 2 +- public/app/plugins/datasource/mysql/datasource.ts | 2 +- public/app/plugins/datasource/mysql/module.ts | 2 +- public/app/plugins/datasource/mysql/query_ctrl.ts | 2 +- public/app/plugins/datasource/opentsdb/query_ctrl.ts | 2 +- public/app/plugins/datasource/postgres/datasource.ts | 2 +- public/app/plugins/datasource/postgres/module.ts | 4 ++-- public/app/plugins/datasource/postgres/query_ctrl.ts | 2 +- public/app/plugins/datasource/testdata/query_ctrl.ts | 2 +- public/app/plugins/panel/gettingstarted/module.ts | 2 +- public/app/plugins/panel/graph/axes_editor.ts | 4 ++-- public/app/plugins/panel/graph/graph.ts | 2 +- public/app/plugins/panel/text/module.ts | 2 +- public/app/routes/routes.ts | 2 +- tslint.json | 1 + 54 files changed, 59 insertions(+), 58 deletions(-) diff --git a/public/app/core/components/form_dropdown/form_dropdown.ts b/public/app/core/components/form_dropdown/form_dropdown.ts index 8f374c225ea..6e863e1cb5d 100644 --- a/public/app/core/components/form_dropdown/form_dropdown.ts +++ b/public/app/core/components/form_dropdown/form_dropdown.ts @@ -36,7 +36,7 @@ export class FormDropdownCtrl { startOpen: any; debounce: number; - /** @ngInject **/ + /** @ngInject */ constructor(private $scope, $element, private $sce, private templateSrv, private $q) { this.inputElement = $element.find('input').first(); this.linkElement = $element.find('a').first(); diff --git a/public/app/core/components/layout_selector/layout_selector.ts b/public/app/core/components/layout_selector/layout_selector.ts index 07f1c8628e9..6fcc768c846 100644 --- a/public/app/core/components/layout_selector/layout_selector.ts +++ b/public/app/core/components/layout_selector/layout_selector.ts @@ -15,7 +15,7 @@ const template = ` export class LayoutSelectorCtrl { mode: string; - /** @ngInject **/ + /** @ngInject */ constructor(private $rootScope) { this.mode = store.get('grafana.list.layout.mode') || 'grid'; } @@ -33,7 +33,7 @@ export class LayoutSelectorCtrl { } } -/** @ngInject **/ +/** @ngInject */ export function layoutSelector() { return { restrict: 'E', @@ -45,7 +45,7 @@ export function layoutSelector() { }; } -/** @ngInject **/ +/** @ngInject */ export function layoutMode($rootScope) { return { restrict: 'A', diff --git a/public/app/core/directives/rebuild_on_change.ts b/public/app/core/directives/rebuild_on_change.ts index edb7c48b482..72b9c05064a 100644 --- a/public/app/core/directives/rebuild_on_change.ts +++ b/public/app/core/directives/rebuild_on_change.ts @@ -18,7 +18,7 @@ function getBlockNodes(nodes) { return blockNodes || nodes; } -/** @ngInject **/ +/** @ngInject */ function rebuildOnChange($animate) { return { multiElement: true, diff --git a/public/app/core/services/popover_srv.ts b/public/app/core/services/popover_srv.ts index 5072d04fa9f..631cc274021 100644 --- a/public/app/core/services/popover_srv.ts +++ b/public/app/core/services/popover_srv.ts @@ -2,7 +2,7 @@ import _ from 'lodash'; import coreModule from 'app/core/core_module'; import Drop from 'tether-drop'; -/** @ngInject **/ +/** @ngInject */ function popoverSrv(this: any, $compile, $rootScope, $timeout) { let openDrop = null; diff --git a/public/app/features/admin/admin.ts b/public/app/features/admin/admin.ts index bdbb887817e..383b50b5d25 100644 --- a/public/app/features/admin/admin.ts +++ b/public/app/features/admin/admin.ts @@ -8,7 +8,7 @@ import coreModule from 'app/core/core_module'; class AdminSettingsCtrl { navModel: any; - /** @ngInject **/ + /** @ngInject */ constructor($scope, backendSrv, navModelSrv) { this.navModel = navModelSrv.getNav('cfg', 'admin', 'server-settings', 1); @@ -21,7 +21,7 @@ class AdminSettingsCtrl { class AdminHomeCtrl { navModel: any; - /** @ngInject **/ + /** @ngInject */ constructor(navModelSrv) { this.navModel = navModelSrv.getNav('cfg', 'admin', 1); } diff --git a/public/app/features/annotations/annotation_tooltip.ts b/public/app/features/annotations/annotation_tooltip.ts index 9d1147aba60..6da6fc4f66d 100644 --- a/public/app/features/annotations/annotation_tooltip.ts +++ b/public/app/features/annotations/annotation_tooltip.ts @@ -3,7 +3,7 @@ import $ from 'jquery'; import coreModule from 'app/core/core_module'; import alertDef from '../alerting/alert_def'; -/** @ngInject **/ +/** @ngInject */ export function annotationTooltipDirective($sanitize, dashboardSrv, contextSrv, $compile) { function sanitizeString(str) { try { diff --git a/public/app/features/annotations/event_editor.ts b/public/app/features/annotations/event_editor.ts index 90c425438ab..a88cc44a251 100644 --- a/public/app/features/annotations/event_editor.ts +++ b/public/app/features/annotations/event_editor.ts @@ -12,7 +12,7 @@ export class EventEditorCtrl { close: any; timeFormated: string; - /** @ngInject **/ + /** @ngInject */ constructor(private annotationsSrv) { this.event.panelId = this.panelCtrl.panel.id; this.event.dashboardId = this.panelCtrl.dashboard.id; diff --git a/public/app/features/dashboard/create_folder_ctrl.ts b/public/app/features/dashboard/create_folder_ctrl.ts index 5c8bd276f76..99b2e8d4853 100644 --- a/public/app/features/dashboard/create_folder_ctrl.ts +++ b/public/app/features/dashboard/create_folder_ctrl.ts @@ -8,7 +8,7 @@ export class CreateFolderCtrl { hasValidationError: boolean; validationError: any; - /** @ngInject **/ + /** @ngInject */ constructor(private backendSrv, private $location, private validationSrv, navModelSrv) { this.navModel = navModelSrv.getNav('dashboards', 'manage-dashboards', 0); } diff --git a/public/app/features/dashboard/repeat_option/repeat_option.ts b/public/app/features/dashboard/repeat_option/repeat_option.ts index 01e1d716fc5..19c28607640 100644 --- a/public/app/features/dashboard/repeat_option/repeat_option.ts +++ b/public/app/features/dashboard/repeat_option/repeat_option.ts @@ -7,7 +7,7 @@ const template = `
    `; -/** @ngInject **/ +/** @ngInject */ function dashRepeatOptionDirective(variableSrv) { return { restrict: 'E', diff --git a/public/app/features/dashboard/share_snapshot_ctrl.ts b/public/app/features/dashboard/share_snapshot_ctrl.ts index c470ddb47dd..2cda493838a 100644 --- a/public/app/features/dashboard/share_snapshot_ctrl.ts +++ b/public/app/features/dashboard/share_snapshot_ctrl.ts @@ -2,7 +2,7 @@ import angular from 'angular'; import _ from 'lodash'; export class ShareSnapshotCtrl { - /** @ngInject **/ + /** @ngInject */ constructor($scope, $rootScope, $location, backendSrv, $timeout, timeSrv) { $scope.snapshot = { name: $scope.dashboard.title, diff --git a/public/app/features/dashboard/time_srv.ts b/public/app/features/dashboard/time_srv.ts index 9435433848e..4bd78ce776d 100644 --- a/public/app/features/dashboard/time_srv.ts +++ b/public/app/features/dashboard/time_srv.ts @@ -13,7 +13,7 @@ export class TimeSrv { timeAtLoad: any; private autoRefreshBlocked: boolean; - /** @ngInject **/ + /** @ngInject */ constructor(private $rootScope, private $timeout, private $location, private timer, private contextSrv) { // default time this.time = { from: '6h', to: 'now' }; diff --git a/public/app/features/org/change_password_ctrl.ts b/public/app/features/org/change_password_ctrl.ts index b84cbecfea7..033ff807721 100644 --- a/public/app/features/org/change_password_ctrl.ts +++ b/public/app/features/org/change_password_ctrl.ts @@ -2,7 +2,7 @@ import angular from 'angular'; import config from 'app/core/config'; export class ChangePasswordCtrl { - /** @ngInject **/ + /** @ngInject */ constructor($scope, backendSrv, $location, navModelSrv) { $scope.command = {}; $scope.authProxyEnabled = config.authProxyEnabled; diff --git a/public/app/features/org/create_team_ctrl.ts b/public/app/features/org/create_team_ctrl.ts index 241e96968a0..d016d85afc0 100644 --- a/public/app/features/org/create_team_ctrl.ts +++ b/public/app/features/org/create_team_ctrl.ts @@ -5,7 +5,7 @@ export default class CreateTeamCtrl { email: string; navModel: any; - /** @ngInject **/ + /** @ngInject */ constructor(private backendSrv, private $location, navModelSrv) { this.navModel = navModelSrv.getNav('cfg', 'teams', 0); } diff --git a/public/app/features/org/new_org_ctrl.ts b/public/app/features/org/new_org_ctrl.ts index 91b16adc113..bc87010c100 100644 --- a/public/app/features/org/new_org_ctrl.ts +++ b/public/app/features/org/new_org_ctrl.ts @@ -2,7 +2,7 @@ import angular from 'angular'; import config from 'app/core/config'; export class NewOrgCtrl { - /** @ngInject **/ + /** @ngInject */ constructor($scope, $http, backendSrv, navModelSrv) { $scope.navModel = navModelSrv.getNav('cfg', 'admin', 'global-orgs', 1); $scope.newOrg = { name: '' }; diff --git a/public/app/features/org/org_api_keys_ctrl.ts b/public/app/features/org/org_api_keys_ctrl.ts index 4ea40b900c7..668d6a86841 100644 --- a/public/app/features/org/org_api_keys_ctrl.ts +++ b/public/app/features/org/org_api_keys_ctrl.ts @@ -1,7 +1,7 @@ import angular from 'angular'; export class OrgApiKeysCtrl { - /** @ngInject **/ + /** @ngInject */ constructor($scope, $http, backendSrv, navModelSrv) { $scope.navModel = navModelSrv.getNav('cfg', 'apikeys', 0); diff --git a/public/app/features/org/org_details_ctrl.ts b/public/app/features/org/org_details_ctrl.ts index 7bc6ee1336b..2ec1b57e170 100644 --- a/public/app/features/org/org_details_ctrl.ts +++ b/public/app/features/org/org_details_ctrl.ts @@ -1,7 +1,7 @@ import angular from 'angular'; export class OrgDetailsCtrl { - /** @ngInject **/ + /** @ngInject */ constructor($scope, $http, backendSrv, contextSrv, navModelSrv) { $scope.init = function() { $scope.getOrgInfo(); diff --git a/public/app/features/org/prefs_control.ts b/public/app/features/org/prefs_control.ts index 6c6ffdf647d..74dde250eec 100644 --- a/public/app/features/org/prefs_control.ts +++ b/public/app/features/org/prefs_control.ts @@ -14,7 +14,7 @@ export class PrefsControlCtrl { ]; themes: any = [{ value: '', text: 'Default' }, { value: 'dark', text: 'Dark' }, { value: 'light', text: 'Light' }]; - /** @ngInject **/ + /** @ngInject */ constructor(private backendSrv, private $location) {} $onInit() { diff --git a/public/app/features/org/profile_ctrl.ts b/public/app/features/org/profile_ctrl.ts index 40ee4d908a1..d394fc4319a 100644 --- a/public/app/features/org/profile_ctrl.ts +++ b/public/app/features/org/profile_ctrl.ts @@ -12,7 +12,7 @@ export class ProfileCtrl { readonlyLoginFields = config.disableLoginForm; navModel: any; - /** @ngInject **/ + /** @ngInject */ constructor(private backendSrv, private contextSrv, private $location, navModelSrv) { this.getUser(); this.getUserTeams(); diff --git a/public/app/features/org/select_org_ctrl.ts b/public/app/features/org/select_org_ctrl.ts index 199d1f8ac94..34cfc9b7df4 100644 --- a/public/app/features/org/select_org_ctrl.ts +++ b/public/app/features/org/select_org_ctrl.ts @@ -2,7 +2,7 @@ import angular from 'angular'; import config from 'app/core/config'; export class SelectOrgCtrl { - /** @ngInject **/ + /** @ngInject */ constructor($scope, backendSrv, contextSrv) { contextSrv.sidemenu = false; diff --git a/public/app/features/org/user_invite_ctrl.ts b/public/app/features/org/user_invite_ctrl.ts index 36dde418b2c..9f3b641035a 100644 --- a/public/app/features/org/user_invite_ctrl.ts +++ b/public/app/features/org/user_invite_ctrl.ts @@ -5,7 +5,7 @@ export class UserInviteCtrl { invite: any; inviteForm: any; - /** @ngInject **/ + /** @ngInject */ constructor(private backendSrv, navModelSrv, private $location) { this.navModel = navModelSrv.getNav('cfg', 'users', 0); diff --git a/public/app/features/panel/metrics_tab.ts b/public/app/features/panel/metrics_tab.ts index e55190bf491..3a1d0abe1c2 100644 --- a/public/app/features/panel/metrics_tab.ts +++ b/public/app/features/panel/metrics_tab.ts @@ -110,7 +110,7 @@ export class MetricsTabCtrl { } } -/** @ngInject **/ +/** @ngInject */ export function metricsTabDirective() { 'use strict'; return { diff --git a/public/app/features/panel/panel_header.ts b/public/app/features/panel/panel_header.ts index 410be0a1890..102f065cff2 100644 --- a/public/app/features/panel/panel_header.ts +++ b/public/app/features/panel/panel_header.ts @@ -80,7 +80,7 @@ function createMenuTemplate(ctrl) { return html; } -/** @ngInject **/ +/** @ngInject */ function panelHeader($compile) { return { restrict: 'E', diff --git a/public/app/features/panel/query_editor_row.ts b/public/app/features/panel/query_editor_row.ts index 211dbcfccb2..fb4c4a5cde6 100644 --- a/public/app/features/panel/query_editor_row.ts +++ b/public/app/features/panel/query_editor_row.ts @@ -87,7 +87,7 @@ export class QueryRowCtrl { } } -/** @ngInject **/ +/** @ngInject */ function queryEditorRowDirective() { return { restrict: 'E', diff --git a/public/app/features/panel/query_troubleshooter.ts b/public/app/features/panel/query_troubleshooter.ts index d9959a05a62..e4d2eb5a302 100644 --- a/public/app/features/panel/query_troubleshooter.ts +++ b/public/app/features/panel/query_troubleshooter.ts @@ -40,7 +40,7 @@ export class QueryTroubleshooterCtrl { mockedResponse: string; jsonExplorer: JsonExplorer; - /** @ngInject **/ + /** @ngInject */ constructor($scope, private $timeout) { this.onRequestErrorEventListener = this.onRequestError.bind(this); this.onRequestResponseEventListener = this.onRequestResponse.bind(this); diff --git a/public/app/features/plugins/plugin_component.ts b/public/app/features/plugins/plugin_component.ts index 8ef20eb54d5..dc55ee2a181 100644 --- a/public/app/features/plugins/plugin_component.ts +++ b/public/app/features/plugins/plugin_component.ts @@ -7,7 +7,7 @@ import { importPluginModule } from './plugin_loader'; import { UnknownPanelCtrl } from 'app/plugins/panel/unknown/module'; -/** @ngInject **/ +/** @ngInject */ function pluginDirectiveLoader($compile, datasourceSrv, $rootScope, $q, $http, $templateCache) { function getTemplate(component) { if (component.template) { diff --git a/public/app/features/styleguide/styleguide.ts b/public/app/features/styleguide/styleguide.ts index 26f14bf9388..4aac194d950 100644 --- a/public/app/features/styleguide/styleguide.ts +++ b/public/app/features/styleguide/styleguide.ts @@ -8,7 +8,7 @@ class StyleGuideCtrl { buttonVariants = ['-']; navModel: any; - /** @ngInject **/ + /** @ngInject */ constructor(private $routeParams, private backendSrv, navModelSrv) { this.navModel = navModelSrv.getNav('cfg', 'admin', 'styleguide', 1); this.theme = config.bootData.user.lightTheme ? 'light' : 'dark'; diff --git a/public/app/features/templating/adhoc_variable.ts b/public/app/features/templating/adhoc_variable.ts index 3e5b2af8b6b..bc157bfd697 100644 --- a/public/app/features/templating/adhoc_variable.ts +++ b/public/app/features/templating/adhoc_variable.ts @@ -15,7 +15,7 @@ export class AdhocVariable implements Variable { skipUrlSync: false, }; - /** @ngInject **/ + /** @ngInject */ constructor(private model) { assignModelProperties(this, model, this.defaults); } diff --git a/public/app/features/templating/constant_variable.ts b/public/app/features/templating/constant_variable.ts index e727c6e98af..dcda31f43a3 100644 --- a/public/app/features/templating/constant_variable.ts +++ b/public/app/features/templating/constant_variable.ts @@ -17,7 +17,7 @@ export class ConstantVariable implements Variable { skipUrlSync: false, }; - /** @ngInject **/ + /** @ngInject */ constructor(private model, private variableSrv) { assignModelProperties(this, model, this.defaults); } diff --git a/public/app/features/templating/custom_variable.ts b/public/app/features/templating/custom_variable.ts index 4490a41a38f..fe383c68077 100644 --- a/public/app/features/templating/custom_variable.ts +++ b/public/app/features/templating/custom_variable.ts @@ -23,7 +23,7 @@ export class CustomVariable implements Variable { skipUrlSync: false, }; - /** @ngInject **/ + /** @ngInject */ constructor(private model, private variableSrv) { assignModelProperties(this, model, this.defaults); } diff --git a/public/app/features/templating/datasource_variable.ts b/public/app/features/templating/datasource_variable.ts index 258fa043e1d..4424720c7f8 100644 --- a/public/app/features/templating/datasource_variable.ts +++ b/public/app/features/templating/datasource_variable.ts @@ -22,7 +22,7 @@ export class DatasourceVariable implements Variable { skipUrlSync: false, }; - /** @ngInject **/ + /** @ngInject */ constructor(private model, private datasourceSrv, private variableSrv, private templateSrv) { assignModelProperties(this, model, this.defaults); this.refresh = 1; diff --git a/public/app/features/templating/editor_ctrl.ts b/public/app/features/templating/editor_ctrl.ts index 1222af7f93c..9dc6468415a 100644 --- a/public/app/features/templating/editor_ctrl.ts +++ b/public/app/features/templating/editor_ctrl.ts @@ -4,7 +4,7 @@ import { variableTypes } from './variable'; import appEvents from 'app/core/app_events'; export class VariableEditorCtrl { - /** @ngInject **/ + /** @ngInject */ constructor($scope, datasourceSrv, variableSrv, templateSrv) { $scope.variableTypes = variableTypes; $scope.ctrl = {}; diff --git a/public/app/features/templating/interval_variable.ts b/public/app/features/templating/interval_variable.ts index e6ee861f828..bb6c2a19e7f 100644 --- a/public/app/features/templating/interval_variable.ts +++ b/public/app/features/templating/interval_variable.ts @@ -28,7 +28,7 @@ export class IntervalVariable implements Variable { skipUrlSync: false, }; - /** @ngInject **/ + /** @ngInject */ constructor(private model, private timeSrv, private templateSrv, private variableSrv) { assignModelProperties(this, model, this.defaults); this.refresh = 2; diff --git a/public/app/features/templating/query_variable.ts b/public/app/features/templating/query_variable.ts index 1b8c3697fe9..e1ffcb837cb 100644 --- a/public/app/features/templating/query_variable.ts +++ b/public/app/features/templating/query_variable.ts @@ -46,7 +46,7 @@ export class QueryVariable implements Variable { skipUrlSync: false, }; - /** @ngInject **/ + /** @ngInject */ constructor(private model, private datasourceSrv, private templateSrv, private variableSrv, private timeSrv) { // copy model properties to this instance assignModelProperties(this, model, this.defaults); diff --git a/public/app/plugins/datasource/cloudwatch/query_ctrl.ts b/public/app/plugins/datasource/cloudwatch/query_ctrl.ts index 0d250935445..55b7786e302 100644 --- a/public/app/plugins/datasource/cloudwatch/query_ctrl.ts +++ b/public/app/plugins/datasource/cloudwatch/query_ctrl.ts @@ -6,7 +6,7 @@ export class CloudWatchQueryCtrl extends QueryCtrl { aliasSyntax: string; - /** @ngInject **/ + /** @ngInject */ constructor($scope, $injector) { super($scope, $injector); this.aliasSyntax = '{{metric}} {{stat}} {{namespace}} {{region}} {{}}'; diff --git a/public/app/plugins/datasource/elasticsearch/query_ctrl.ts b/public/app/plugins/datasource/elasticsearch/query_ctrl.ts index bdfdbda8215..422d35dd277 100644 --- a/public/app/plugins/datasource/elasticsearch/query_ctrl.ts +++ b/public/app/plugins/datasource/elasticsearch/query_ctrl.ts @@ -12,7 +12,7 @@ export class ElasticQueryCtrl extends QueryCtrl { esVersion: any; rawQueryOld: string; - /** @ngInject **/ + /** @ngInject */ constructor($scope, $injector, private $rootScope, private uiSegmentSrv) { super($scope, $injector); diff --git a/public/app/plugins/datasource/graphite/query_ctrl.ts b/public/app/plugins/datasource/graphite/query_ctrl.ts index 60c6b3c262a..fa908c5e955 100644 --- a/public/app/plugins/datasource/graphite/query_ctrl.ts +++ b/public/app/plugins/datasource/graphite/query_ctrl.ts @@ -19,7 +19,7 @@ export class GraphiteQueryCtrl extends QueryCtrl { supportsTags: boolean; paused: boolean; - /** @ngInject **/ + /** @ngInject */ constructor($scope, $injector, private uiSegmentSrv, private templateSrv, $timeout) { super($scope, $injector); this.supportsTags = this.datasource.supportsTags; diff --git a/public/app/plugins/datasource/influxdb/query_ctrl.ts b/public/app/plugins/datasource/influxdb/query_ctrl.ts index ae62fcc1b2a..4a9310c63d1 100644 --- a/public/app/plugins/datasource/influxdb/query_ctrl.ts +++ b/public/app/plugins/datasource/influxdb/query_ctrl.ts @@ -19,7 +19,7 @@ export class InfluxQueryCtrl extends QueryCtrl { measurementSegment: any; removeTagFilterSegment: any; - /** @ngInject **/ + /** @ngInject */ constructor($scope, $injector, private templateSrv, private $q, private uiSegmentSrv) { super($scope, $injector); this.target = this.target; diff --git a/public/app/plugins/datasource/mssql/datasource.ts b/public/app/plugins/datasource/mssql/datasource.ts index f30ea4c97fe..fc497b2c274 100644 --- a/public/app/plugins/datasource/mssql/datasource.ts +++ b/public/app/plugins/datasource/mssql/datasource.ts @@ -6,7 +6,7 @@ export class MssqlDatasource { name: any; responseParser: ResponseParser; - /** @ngInject **/ + /** @ngInject */ constructor(instanceSettings, private backendSrv, private $q, private templateSrv) { this.name = instanceSettings.name; this.id = instanceSettings.id; diff --git a/public/app/plugins/datasource/mssql/module.ts b/public/app/plugins/datasource/mssql/module.ts index a2e1e923bc6..478ecadcb3e 100644 --- a/public/app/plugins/datasource/mssql/module.ts +++ b/public/app/plugins/datasource/mssql/module.ts @@ -21,7 +21,7 @@ class MssqlAnnotationsQueryCtrl { annotation: any; - /** @ngInject **/ + /** @ngInject */ constructor() { this.annotation.rawQuery = this.annotation.rawQuery || defaultQuery; } diff --git a/public/app/plugins/datasource/mssql/query_ctrl.ts b/public/app/plugins/datasource/mssql/query_ctrl.ts index 1b64a571c6c..7598ca292fe 100644 --- a/public/app/plugins/datasource/mssql/query_ctrl.ts +++ b/public/app/plugins/datasource/mssql/query_ctrl.ts @@ -33,7 +33,7 @@ export class MssqlQueryCtrl extends QueryCtrl { lastQueryError: string; showHelp: boolean; - /** @ngInject **/ + /** @ngInject */ constructor($scope, $injector) { super($scope, $injector); diff --git a/public/app/plugins/datasource/mysql/datasource.ts b/public/app/plugins/datasource/mysql/datasource.ts index e41417e155c..eca223f2d6d 100644 --- a/public/app/plugins/datasource/mysql/datasource.ts +++ b/public/app/plugins/datasource/mysql/datasource.ts @@ -6,7 +6,7 @@ export class MysqlDatasource { name: any; responseParser: ResponseParser; - /** @ngInject **/ + /** @ngInject */ constructor(instanceSettings, private backendSrv, private $q, private templateSrv) { this.name = instanceSettings.name; this.id = instanceSettings.id; diff --git a/public/app/plugins/datasource/mysql/module.ts b/public/app/plugins/datasource/mysql/module.ts index bb5b5bd9056..2d8caf17af4 100644 --- a/public/app/plugins/datasource/mysql/module.ts +++ b/public/app/plugins/datasource/mysql/module.ts @@ -20,7 +20,7 @@ class MysqlAnnotationsQueryCtrl { annotation: any; - /** @ngInject **/ + /** @ngInject */ constructor() { this.annotation.rawQuery = this.annotation.rawQuery || defaultQuery; } diff --git a/public/app/plugins/datasource/mysql/query_ctrl.ts b/public/app/plugins/datasource/mysql/query_ctrl.ts index 1de1fb768ad..ced4d6e8b13 100644 --- a/public/app/plugins/datasource/mysql/query_ctrl.ts +++ b/public/app/plugins/datasource/mysql/query_ctrl.ts @@ -31,7 +31,7 @@ export class MysqlQueryCtrl extends QueryCtrl { lastQueryError: string; showHelp: boolean; - /** @ngInject **/ + /** @ngInject */ constructor($scope, $injector) { super($scope, $injector); diff --git a/public/app/plugins/datasource/opentsdb/query_ctrl.ts b/public/app/plugins/datasource/opentsdb/query_ctrl.ts index e00a01ebd9f..4c8a0ed8d12 100644 --- a/public/app/plugins/datasource/opentsdb/query_ctrl.ts +++ b/public/app/plugins/datasource/opentsdb/query_ctrl.ts @@ -19,7 +19,7 @@ export class OpenTsQueryCtrl extends QueryCtrl { addTagMode: boolean; addFilterMode: boolean; - /** @ngInject **/ + /** @ngInject */ constructor($scope, $injector) { super($scope, $injector); diff --git a/public/app/plugins/datasource/postgres/datasource.ts b/public/app/plugins/datasource/postgres/datasource.ts index 02aa947fd1c..678327a4a86 100644 --- a/public/app/plugins/datasource/postgres/datasource.ts +++ b/public/app/plugins/datasource/postgres/datasource.ts @@ -6,7 +6,7 @@ export class PostgresDatasource { name: any; responseParser: ResponseParser; - /** @ngInject **/ + /** @ngInject */ constructor(instanceSettings, private backendSrv, private $q, private templateSrv) { this.name = instanceSettings.name; this.id = instanceSettings.id; diff --git a/public/app/plugins/datasource/postgres/module.ts b/public/app/plugins/datasource/postgres/module.ts index a24971fa1a1..a4266183bcf 100644 --- a/public/app/plugins/datasource/postgres/module.ts +++ b/public/app/plugins/datasource/postgres/module.ts @@ -6,7 +6,7 @@ class PostgresConfigCtrl { current: any; - /** @ngInject **/ + /** @ngInject */ constructor($scope) { this.current.jsonData.sslmode = this.current.jsonData.sslmode || 'verify-full'; } @@ -27,7 +27,7 @@ class PostgresAnnotationsQueryCtrl { annotation: any; - /** @ngInject **/ + /** @ngInject */ constructor() { this.annotation.rawQuery = this.annotation.rawQuery || defaultQuery; } diff --git a/public/app/plugins/datasource/postgres/query_ctrl.ts b/public/app/plugins/datasource/postgres/query_ctrl.ts index a9073de22cf..fceca1e2037 100644 --- a/public/app/plugins/datasource/postgres/query_ctrl.ts +++ b/public/app/plugins/datasource/postgres/query_ctrl.ts @@ -31,7 +31,7 @@ export class PostgresQueryCtrl extends QueryCtrl { lastQueryError: string; showHelp: boolean; - /** @ngInject **/ + /** @ngInject */ constructor($scope, $injector) { super($scope, $injector); diff --git a/public/app/plugins/datasource/testdata/query_ctrl.ts b/public/app/plugins/datasource/testdata/query_ctrl.ts index dd5f59c0a5a..7a40d264f64 100644 --- a/public/app/plugins/datasource/testdata/query_ctrl.ts +++ b/public/app/plugins/datasource/testdata/query_ctrl.ts @@ -12,7 +12,7 @@ export class TestDataQueryCtrl extends QueryCtrl { newPointTime: any; selectedPoint: any; - /** @ngInject **/ + /** @ngInject */ constructor($scope, $injector, private backendSrv) { super($scope, $injector); diff --git a/public/app/plugins/panel/gettingstarted/module.ts b/public/app/plugins/panel/gettingstarted/module.ts index ebefad8af69..84f367a9430 100644 --- a/public/app/plugins/panel/gettingstarted/module.ts +++ b/public/app/plugins/panel/gettingstarted/module.ts @@ -8,7 +8,7 @@ class GettingStartedPanelCtrl extends PanelCtrl { stepIndex: number; steps: any; - /** @ngInject **/ + /** @ngInject */ constructor($scope, $injector, private backendSrv, datasourceSrv, private $q) { super($scope, $injector); diff --git a/public/app/plugins/panel/graph/axes_editor.ts b/public/app/plugins/panel/graph/axes_editor.ts index 4230acb571b..3d2dd4acbc5 100644 --- a/public/app/plugins/panel/graph/axes_editor.ts +++ b/public/app/plugins/panel/graph/axes_editor.ts @@ -9,7 +9,7 @@ export class AxesEditorCtrl { xAxisStatOptions: any; xNameSegment: any; - /** @ngInject **/ + /** @ngInject */ constructor(private $scope, private $q) { this.panelCtrl = $scope.ctrl; this.panel = this.panelCtrl.panel; @@ -76,7 +76,7 @@ export class AxesEditorCtrl { } } -/** @ngInject **/ +/** @ngInject */ export function axesEditorComponent() { 'use strict'; return { diff --git a/public/app/plugins/panel/graph/graph.ts b/public/app/plugins/panel/graph/graph.ts index 7dec2df1130..31adaac0c5f 100755 --- a/public/app/plugins/panel/graph/graph.ts +++ b/public/app/plugins/panel/graph/graph.ts @@ -735,7 +735,7 @@ class GraphElement { } } -/** @ngInject **/ +/** @ngInject */ function graphDirective(timeSrv, popoverSrv, contextSrv) { return { restrict: 'A', diff --git a/public/app/plugins/panel/text/module.ts b/public/app/plugins/panel/text/module.ts index 684bba124fc..ed446d1699b 100644 --- a/public/app/plugins/panel/text/module.ts +++ b/public/app/plugins/panel/text/module.ts @@ -14,7 +14,7 @@ export class TextPanelCtrl extends PanelCtrl { content: '# title', }; - /** @ngInject **/ + /** @ngInject */ constructor($scope, $injector, private templateSrv, private $sce) { super($scope, $injector); diff --git a/public/app/routes/routes.ts b/public/app/routes/routes.ts index d12711aca5b..55d76dc7239 100644 --- a/public/app/routes/routes.ts +++ b/public/app/routes/routes.ts @@ -8,7 +8,7 @@ import FolderPermissions from 'app/containers/ManageDashboards/FolderPermissions import TeamPages from 'app/containers/Teams/TeamPages'; import TeamList from 'app/containers/Teams/TeamList'; -/** @ngInject **/ +/** @ngInject */ export function setupAngularRoutes($routeProvider, $locationProvider) { $locationProvider.html5Mode(true); diff --git a/tslint.json b/tslint.json index e9caf1d7b38..0af9d285f64 100644 --- a/tslint.json +++ b/tslint.json @@ -25,6 +25,7 @@ "eofline": true, "forin": false, "indent": [true, "spaces", 2], + "jsdoc-format": true, "label-position": true, "max-line-length": [true, 150], "member-access": [true, "no-public"], From 593cc5380f6ac4fe14c8fc1c45daccfb5ffe3dc8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Fri, 31 Aug 2018 09:42:32 -0700 Subject: [PATCH 577/786] wip: redux refactor --- public/app/features/alerting/apis/index.ts | 52 ++++++++++++ .../containers}/AlertRuleList.test.tsx | 0 .../alerting/containers}/AlertRuleList.tsx | 83 +++++++++++++------ .../__snapshots__/AlertRuleList.test.tsx.snap | 0 .../ServerStats.test.tsx | 0 .../ServerStats.tsx | 0 .../__snapshots__/ServerStats.test.tsx.snap | 0 .../{server-stats => serverStats}/api.ts | 0 public/app/routes/routes.ts | 4 +- 9 files changed, 111 insertions(+), 28 deletions(-) create mode 100644 public/app/features/alerting/apis/index.ts rename public/app/{containers/AlertRuleList => features/alerting/containers}/AlertRuleList.test.tsx (100%) rename public/app/{containers/AlertRuleList => features/alerting/containers}/AlertRuleList.tsx (73%) rename public/app/{containers/AlertRuleList => features/alerting/containers}/__snapshots__/AlertRuleList.test.tsx.snap (100%) rename public/app/features/{server-stats => serverStats}/ServerStats.test.tsx (100%) rename public/app/features/{server-stats => serverStats}/ServerStats.tsx (100%) rename public/app/features/{server-stats => serverStats}/__snapshots__/ServerStats.test.tsx.snap (100%) rename public/app/features/{server-stats => serverStats}/api.ts (100%) diff --git a/public/app/features/alerting/apis/index.ts b/public/app/features/alerting/apis/index.ts new file mode 100644 index 00000000000..ebfdbd34024 --- /dev/null +++ b/public/app/features/alerting/apis/index.ts @@ -0,0 +1,52 @@ +import { getBackendSrv } from 'app/core/services/backend_srv'; +import alertDef from '../alert_def'; +import moment from 'moment'; + +export interface AlertRule { + id: number; + dashboardId: number; + panelId: number; + name: string; + state: string; + stateText: string; + stateIcon: string; + stateClass: string; + stateAge: string; + info?: string; + url: string; +} + +export function setStateFields(rule, state) { + const stateModel = alertDef.getStateDisplayModel(state); + rule.state = state; + rule.stateText = stateModel.text; + rule.stateIcon = stateModel.iconClass; + rule.stateClass = stateModel.stateClass; + rule.stateAge = moment(rule.newStateDate) + .fromNow() + .replace(' ago', ''); +} + +export const getAlertRules = async (): Promise => { + try { + const rules = await getBackendSrv().get('/api/alerts', {}); + + for (const rule of rules) { + setStateFields(rule, rule.state); + + if (rule.state !== 'paused') { + if (rule.executionError) { + rule.info = 'Execution Error: ' + rule.executionError; + } + if (rule.evalData && rule.evalData.noData) { + rule.info = 'Query returned no data'; + } + } + } + + return rules; + } catch (error) { + console.error(error); + throw error; + } +}; diff --git a/public/app/containers/AlertRuleList/AlertRuleList.test.tsx b/public/app/features/alerting/containers/AlertRuleList.test.tsx similarity index 100% rename from public/app/containers/AlertRuleList/AlertRuleList.test.tsx rename to public/app/features/alerting/containers/AlertRuleList.test.tsx diff --git a/public/app/containers/AlertRuleList/AlertRuleList.tsx b/public/app/features/alerting/containers/AlertRuleList.tsx similarity index 73% rename from public/app/containers/AlertRuleList/AlertRuleList.tsx rename to public/app/features/alerting/containers/AlertRuleList.tsx index 668136dee6f..665b1508e3e 100644 --- a/public/app/containers/AlertRuleList/AlertRuleList.tsx +++ b/public/app/features/alerting/containers/AlertRuleList.tsx @@ -1,16 +1,23 @@ -import React from 'react'; +import React, { PureComponent } from 'react'; import { hot } from 'react-hot-loader'; +import { connect } from 'react-redux'; import classNames from 'classnames'; -import { inject, observer } from 'mobx-react'; import PageHeader from 'app/core/components/PageHeader/PageHeader'; -import { AlertRule } from 'app/stores/AlertListStore/AlertListStore'; import appEvents from 'app/core/app_events'; -import ContainerProps from 'app/containers/ContainerProps'; import Highlighter from 'react-highlight-words'; +import { initNav } from 'app/core/actions'; +import { ContainerProps } from 'app/types'; +import { getAlertRules, AlertRule } from '../apis'; -@inject('view', 'nav', 'alertList') -@observer -export class AlertRuleList extends React.Component { +interface Props extends ContainerProps {} + +interface State { + rules: AlertRule[]; + search: string; + stateFilter: string; +} + +export class AlertRuleList extends PureComponent { stateFilters = [ { text: 'All', value: 'all' }, { text: 'OK', value: 'ok' }, @@ -23,19 +30,35 @@ export class AlertRuleList extends React.Component { constructor(props) { super(props); - this.props.nav.load('alerting', 'alert-list'); + this.state = { + rules: [], + search: '', + stateFilter: '', + }; + + this.props.initNav('alerting', 'alert-list'); + } + + componentDidMount() { this.fetchRules(); } onStateFilterChanged = evt => { - this.props.view.updateQuery({ state: evt.target.value }); - this.fetchRules(); + // this.props.view.updateQuery({ state: evt.target.value }); + // this.fetchRules(); }; - fetchRules() { - this.props.alertList.loadRules({ - state: this.props.view.query.get('state') || 'all', - }); + async fetchRules() { + try { + const rules = await getAlertRules(); + this.setState({ rules }); + } catch (error) { + console.error(error); + } + + // this.props.alertList.loadRules({ + // state: this.props.view.query.get('state') || 'all', + // }); } onOpenHowTo = () => { @@ -47,15 +70,16 @@ export class AlertRuleList extends React.Component { }; onSearchQueryChange = evt => { - this.props.alertList.setSearchQuery(evt.target.value); + // this.props.alertList.setSearchQuery(evt.target.value); }; render() { - const { nav, alertList } = this.props; + const { navModel } = this.props; + const { rules, search, stateFilter } = this.state; return (
    - +
    @@ -64,7 +88,7 @@ export class AlertRuleList extends React.Component { type="text" className="gf-form-input" placeholder="Search alerts" - value={alertList.search} + value={search} onChange={this.onSearchQueryChange} /> @@ -74,7 +98,7 @@ export class AlertRuleList extends React.Component {
    - {this.stateFilters.map(AlertStateFilterOption)}
    @@ -89,8 +113,8 @@ export class AlertRuleList extends React.Component {
      - {alertList.filteredRules.map(rule => ( - + {rules.map(rule => ( + ))}
    @@ -113,10 +137,9 @@ export interface AlertRuleItemProps { search: string; } -@observer export class AlertRuleItem extends React.Component { toggleState = () => { - this.props.rule.togglePaused(); + // this.props.rule.togglePaused(); }; renderText(text: string) { @@ -134,8 +157,8 @@ export class AlertRuleItem extends React.Component { const stateClass = classNames({ fa: true, - 'fa-play': rule.isPaused, - 'fa-pause': !rule.isPaused, + 'fa-play': rule.state === 'paused', + 'fa-pause': rule.state !== 'paused', }); const ruleUrl = `${rule.url}?panelId=${rule.panelId}&fullscreen=true&edit=true&tab=alert`; @@ -175,4 +198,12 @@ export class AlertRuleItem extends React.Component { } } -export default hot(module)(AlertRuleList); +const mapStateToProps = state => ({ + navModel: state.navModel, +}); + +const mapDispatchToProps = { + initNav, +}; + +export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(AlertRuleList)); diff --git a/public/app/containers/AlertRuleList/__snapshots__/AlertRuleList.test.tsx.snap b/public/app/features/alerting/containers/__snapshots__/AlertRuleList.test.tsx.snap similarity index 100% rename from public/app/containers/AlertRuleList/__snapshots__/AlertRuleList.test.tsx.snap rename to public/app/features/alerting/containers/__snapshots__/AlertRuleList.test.tsx.snap diff --git a/public/app/features/server-stats/ServerStats.test.tsx b/public/app/features/serverStats/ServerStats.test.tsx similarity index 100% rename from public/app/features/server-stats/ServerStats.test.tsx rename to public/app/features/serverStats/ServerStats.test.tsx diff --git a/public/app/features/server-stats/ServerStats.tsx b/public/app/features/serverStats/ServerStats.tsx similarity index 100% rename from public/app/features/server-stats/ServerStats.tsx rename to public/app/features/serverStats/ServerStats.tsx diff --git a/public/app/features/server-stats/__snapshots__/ServerStats.test.tsx.snap b/public/app/features/serverStats/__snapshots__/ServerStats.test.tsx.snap similarity index 100% rename from public/app/features/server-stats/__snapshots__/ServerStats.test.tsx.snap rename to public/app/features/serverStats/__snapshots__/ServerStats.test.tsx.snap diff --git a/public/app/features/server-stats/api.ts b/public/app/features/serverStats/api.ts similarity index 100% rename from public/app/features/server-stats/api.ts rename to public/app/features/serverStats/api.ts diff --git a/public/app/routes/routes.ts b/public/app/routes/routes.ts index 7fcab26645f..cf45176ecf7 100644 --- a/public/app/routes/routes.ts +++ b/public/app/routes/routes.ts @@ -1,8 +1,8 @@ import './dashboard_loaders'; import './ReactContainer'; -import ServerStats from 'app/features/server-stats/ServerStats'; -import AlertRuleList from 'app/containers/AlertRuleList/AlertRuleList'; +import ServerStats from 'app/features/serverStats/ServerStats'; +import AlertRuleList from 'app/features/alerting/containers/AlertRuleList'; import FolderSettings from 'app/containers/ManageDashboards/FolderSettings'; import FolderPermissions from 'app/containers/ManageDashboards/FolderPermissions'; import TeamPages from 'app/containers/Teams/TeamPages'; From 2c85e44ab785681e35bdb855ff650cb148de84f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Fri, 31 Aug 2018 09:49:02 -0700 Subject: [PATCH 578/786] wip: moveing things around --- .../features/serverStats/ServerStats.test.tsx | 30 ---- .../app/features/serverStats/ServerStats.tsx | 78 -------- .../__snapshots__/ServerStats.test.tsx.snap | 170 ------------------ public/app/features/serverStats/api.ts | 26 --- public/app/routes/routes.ts | 2 +- 5 files changed, 1 insertion(+), 305 deletions(-) delete mode 100644 public/app/features/serverStats/ServerStats.test.tsx delete mode 100644 public/app/features/serverStats/ServerStats.tsx delete mode 100644 public/app/features/serverStats/__snapshots__/ServerStats.test.tsx.snap delete mode 100644 public/app/features/serverStats/api.ts diff --git a/public/app/features/serverStats/ServerStats.test.tsx b/public/app/features/serverStats/ServerStats.test.tsx deleted file mode 100644 index a329a47527d..00000000000 --- a/public/app/features/serverStats/ServerStats.test.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import React from 'react'; -import renderer from 'react-test-renderer'; -import { ServerStats } from './ServerStats'; -import { RootStore } from 'app/stores/RootStore/RootStore'; -import { backendSrv, createNavTree } from 'test/mocks/common'; - -describe('ServerStats', () => { - it('Should render table with stats', done => { - backendSrv.get.mockReturnValue( - Promise.resolve({ - dashboards: 10, - }) - ); - - const store = RootStore.create( - {}, - { - backendSrv: backendSrv, - navTree: createNavTree('cfg', 'admin', 'server-stats'), - } - ); - - const page = renderer.create(); - - setTimeout(() => { - expect(page.toJSON()).toMatchSnapshot(); - done(); - }); - }); -}); diff --git a/public/app/features/serverStats/ServerStats.tsx b/public/app/features/serverStats/ServerStats.tsx deleted file mode 100644 index da1fb6e76f7..00000000000 --- a/public/app/features/serverStats/ServerStats.tsx +++ /dev/null @@ -1,78 +0,0 @@ -import React from 'react'; -import { hot } from 'react-hot-loader'; -import { connect } from 'react-redux'; -import { initNav } from 'app/core/actions'; -import { ContainerProps } from 'app/types'; -import { getServerStats, ServerStat } from './api'; -import PageHeader from 'app/core/components/PageHeader/PageHeader'; - -interface Props extends ContainerProps { - getServerStats: () => Promise; -} - -interface State { - stats: ServerStat[]; -} - -export class ServerStats extends React.Component { - constructor(props) { - super(props); - - this.state = { - stats: [], - }; - - this.props.initNav('cfg', 'admin', 'server-stats'); - } - - async componentDidMount() { - try { - const stats = await this.props.getServerStats(); - this.setState({ stats }); - } catch (error) { - console.error(error); - } - } - - render() { - const { navModel } = this.props; - const { stats } = this.state; - - return ( -
    - -
    - - - - - - - - {stats.map(StatItem)} -
    NameValue
    -
    -
    - ); - } -} - -function StatItem(stat: ServerStat) { - return ( - - {stat.name} - {stat.value} - - ); -} - -const mapStateToProps = state => ({ - navModel: state.navModel, - getServerStats: getServerStats, -}); - -const mapDispatchToProps = { - initNav, -}; - -export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(ServerStats)); diff --git a/public/app/features/serverStats/__snapshots__/ServerStats.test.tsx.snap b/public/app/features/serverStats/__snapshots__/ServerStats.test.tsx.snap deleted file mode 100644 index eac793ca2ca..00000000000 --- a/public/app/features/serverStats/__snapshots__/ServerStats.test.tsx.snap +++ /dev/null @@ -1,170 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`ServerStats Should render table with stats 1`] = ` -
    -
    -
    -
    -
    - - - - -
    -

    - admin-Text -

    - -
    -
    - -
    -
    -
    -
    - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    - Name - - Value -
    - Total dashboards - - 10 -
    - Total users - - 0 -
    - Active users (seen last 30 days) - - 0 -
    - Total orgs - - 0 -
    - Total playlists - - 0 -
    - Total snapshots - - 0 -
    - Total dashboard tags - - 0 -
    - Total starred dashboards - - 0 -
    - Total alerts - - 0 -
    -
    -
    -`; diff --git a/public/app/features/serverStats/api.ts b/public/app/features/serverStats/api.ts deleted file mode 100644 index 888cfd4f58f..00000000000 --- a/public/app/features/serverStats/api.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { getBackendSrv } from 'app/core/services/backend_srv'; - -export interface ServerStat { - name: string; - value: string; -} - -export const getServerStats = async (): Promise => { - try { - const res = await getBackendSrv().get('api/admin/stats'); - return [ - { name: 'Total users', value: res.users }, - { name: 'Total dashboards', value: res.dashboards }, - { name: 'Active users (seen last 30 days)', value: res.activeUsers }, - { name: 'Total orgs', value: res.orgs }, - { name: 'Total playlists', value: res.playlists }, - { name: 'Total snapshots', value: res.snapshots }, - { name: 'Total dashboard tags', value: res.tags }, - { name: 'Total starred dashboards', value: res.stars }, - { name: 'Total alerts', value: res.alerts }, - ]; - } catch (error) { - console.error(error); - throw error; - } -}; diff --git a/public/app/routes/routes.ts b/public/app/routes/routes.ts index cf45176ecf7..7b1e223afe5 100644 --- a/public/app/routes/routes.ts +++ b/public/app/routes/routes.ts @@ -1,7 +1,7 @@ import './dashboard_loaders'; import './ReactContainer'; -import ServerStats from 'app/features/serverStats/ServerStats'; +import ServerStats from 'app/features/admin/containers/ServerStats'; import AlertRuleList from 'app/features/alerting/containers/AlertRuleList'; import FolderSettings from 'app/containers/ManageDashboards/FolderSettings'; import FolderPermissions from 'app/containers/ManageDashboards/FolderPermissions'; From 6efe9da10f99448740609845550211c361117086 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Fri, 31 Aug 2018 09:49:32 -0700 Subject: [PATCH 579/786] wip: moving things around --- public/app/features/admin/apis/index.ts | 26 +++ .../admin/containers/ServerStats.test.tsx | 30 ++++ .../features/admin/containers/ServerStats.tsx | 78 ++++++++ .../__snapshots__/ServerStats.test.tsx.snap | 170 ++++++++++++++++++ 4 files changed, 304 insertions(+) create mode 100644 public/app/features/admin/apis/index.ts create mode 100644 public/app/features/admin/containers/ServerStats.test.tsx create mode 100644 public/app/features/admin/containers/ServerStats.tsx create mode 100644 public/app/features/admin/containers/__snapshots__/ServerStats.test.tsx.snap diff --git a/public/app/features/admin/apis/index.ts b/public/app/features/admin/apis/index.ts new file mode 100644 index 00000000000..888cfd4f58f --- /dev/null +++ b/public/app/features/admin/apis/index.ts @@ -0,0 +1,26 @@ +import { getBackendSrv } from 'app/core/services/backend_srv'; + +export interface ServerStat { + name: string; + value: string; +} + +export const getServerStats = async (): Promise => { + try { + const res = await getBackendSrv().get('api/admin/stats'); + return [ + { name: 'Total users', value: res.users }, + { name: 'Total dashboards', value: res.dashboards }, + { name: 'Active users (seen last 30 days)', value: res.activeUsers }, + { name: 'Total orgs', value: res.orgs }, + { name: 'Total playlists', value: res.playlists }, + { name: 'Total snapshots', value: res.snapshots }, + { name: 'Total dashboard tags', value: res.tags }, + { name: 'Total starred dashboards', value: res.stars }, + { name: 'Total alerts', value: res.alerts }, + ]; + } catch (error) { + console.error(error); + throw error; + } +}; diff --git a/public/app/features/admin/containers/ServerStats.test.tsx b/public/app/features/admin/containers/ServerStats.test.tsx new file mode 100644 index 00000000000..a329a47527d --- /dev/null +++ b/public/app/features/admin/containers/ServerStats.test.tsx @@ -0,0 +1,30 @@ +import React from 'react'; +import renderer from 'react-test-renderer'; +import { ServerStats } from './ServerStats'; +import { RootStore } from 'app/stores/RootStore/RootStore'; +import { backendSrv, createNavTree } from 'test/mocks/common'; + +describe('ServerStats', () => { + it('Should render table with stats', done => { + backendSrv.get.mockReturnValue( + Promise.resolve({ + dashboards: 10, + }) + ); + + const store = RootStore.create( + {}, + { + backendSrv: backendSrv, + navTree: createNavTree('cfg', 'admin', 'server-stats'), + } + ); + + const page = renderer.create(); + + setTimeout(() => { + expect(page.toJSON()).toMatchSnapshot(); + done(); + }); + }); +}); diff --git a/public/app/features/admin/containers/ServerStats.tsx b/public/app/features/admin/containers/ServerStats.tsx new file mode 100644 index 00000000000..7e96dcf4e0e --- /dev/null +++ b/public/app/features/admin/containers/ServerStats.tsx @@ -0,0 +1,78 @@ +import React from 'react'; +import { hot } from 'react-hot-loader'; +import { connect } from 'react-redux'; +import { initNav } from 'app/core/actions'; +import { ContainerProps } from 'app/types'; +import { getServerStats, ServerStat } from '../apis'; +import PageHeader from 'app/core/components/PageHeader/PageHeader'; + +interface Props extends ContainerProps { + getServerStats: () => Promise; +} + +interface State { + stats: ServerStat[]; +} + +export class ServerStats extends React.Component { + constructor(props) { + super(props); + + this.state = { + stats: [], + }; + + this.props.initNav('cfg', 'admin', 'server-stats'); + } + + async componentDidMount() { + try { + const stats = await this.props.getServerStats(); + this.setState({ stats }); + } catch (error) { + console.error(error); + } + } + + render() { + const { navModel } = this.props; + const { stats } = this.state; + + return ( +
    + +
    + + + + + + + + {stats.map(StatItem)} +
    NameValue
    +
    +
    + ); + } +} + +function StatItem(stat: ServerStat) { + return ( + + {stat.name} + {stat.value} + + ); +} + +const mapStateToProps = state => ({ + navModel: state.navModel, + getServerStats: getServerStats, +}); + +const mapDispatchToProps = { + initNav, +}; + +export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(ServerStats)); diff --git a/public/app/features/admin/containers/__snapshots__/ServerStats.test.tsx.snap b/public/app/features/admin/containers/__snapshots__/ServerStats.test.tsx.snap new file mode 100644 index 00000000000..eac793ca2ca --- /dev/null +++ b/public/app/features/admin/containers/__snapshots__/ServerStats.test.tsx.snap @@ -0,0 +1,170 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ServerStats Should render table with stats 1`] = ` +
    +
    +
    +
    +
    + + + + +
    +

    + admin-Text +

    + +
    +
    + +
    +
    +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + Name + + Value +
    + Total dashboards + + 10 +
    + Total users + + 0 +
    + Active users (seen last 30 days) + + 0 +
    + Total orgs + + 0 +
    + Total playlists + + 0 +
    + Total snapshots + + 0 +
    + Total dashboard tags + + 0 +
    + Total starred dashboards + + 0 +
    + Total alerts + + 0 +
    +
    +
    +`; From de456f8b7356fe0de98d1d8a86f25deccba6627f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Fri, 31 Aug 2018 13:16:20 -0700 Subject: [PATCH 580/786] wip: solid progress on redux -> angular location bridge update --- public/app/core/actions/index.ts | 3 +- public/app/core/actions/location.ts | 13 +++++++ public/app/core/reducers/index.ts | 2 ++ public/app/core/reducers/location.ts | 35 +++++++++++++++++++ public/app/core/services/bridge_srv.ts | 33 +++++++++++++++++ .../alerting/containers/AlertRuleList.tsx | 15 ++++---- public/app/types/index.ts | 3 +- public/app/types/location.ts | 15 ++++++++ 8 files changed, 111 insertions(+), 8 deletions(-) create mode 100644 public/app/core/actions/location.ts create mode 100644 public/app/core/reducers/location.ts create mode 100644 public/app/types/location.ts diff --git a/public/app/core/actions/index.ts b/public/app/core/actions/index.ts index 3c23dbbbe54..7a965f82dd1 100644 --- a/public/app/core/actions/index.ts +++ b/public/app/core/actions/index.ts @@ -1,3 +1,4 @@ import { initNav } from './navModel'; +import { updateLocation } from './location'; -export { initNav }; +export { initNav, updateLocation }; diff --git a/public/app/core/actions/location.ts b/public/app/core/actions/location.ts new file mode 100644 index 00000000000..6f7ac67363e --- /dev/null +++ b/public/app/core/actions/location.ts @@ -0,0 +1,13 @@ +import { LocationUpdate } from 'app/types'; + +export type Action = UpdateLocationAction; + +export interface UpdateLocationAction { + type: 'UPDATE_LOCATION'; + payload: LocationUpdate; +} + +export const updateLocation = (location: LocationUpdate): UpdateLocationAction => ({ + type: 'UPDATE_LOCATION', + payload: location, +}); diff --git a/public/app/core/reducers/index.ts b/public/app/core/reducers/index.ts index 0779111c16e..98f796981e4 100644 --- a/public/app/core/reducers/index.ts +++ b/public/app/core/reducers/index.ts @@ -1,5 +1,7 @@ import navModel from './navModel'; +import location from './location'; export default { navModel, + location, }; diff --git a/public/app/core/reducers/location.ts b/public/app/core/reducers/location.ts new file mode 100644 index 00000000000..5676c82844a --- /dev/null +++ b/public/app/core/reducers/location.ts @@ -0,0 +1,35 @@ +import { Action } from 'app/core/actions/location'; +import { LocationState, UrlQueryMap } from 'app/types'; +import { toUrlParams } from 'app/core/utils/url'; + +export const initialState: LocationState = { + url: '', + path: '', + query: {}, + routeParams: {}, +}; + +function renderUrl(path: string, query: UrlQueryMap): string { + if (Object.keys(query).length > 0) { + path += '?' + toUrlParams(query); + } + return path; +} + +const routerReducer = (state = initialState, action: Action): LocationState => { + switch (action.type) { + case 'UPDATE_LOCATION': { + const { path, query, routeParams } = action.payload; + return { + url: renderUrl(path || state.path, query), + path: path || state.path, + query: query || state.query, + routeParams: routeParams || state.routeParams, + }; + } + } + + return state; +}; + +export default routerReducer; diff --git a/public/app/core/services/bridge_srv.ts b/public/app/core/services/bridge_srv.ts index bdc2976a94c..29326794ac6 100644 --- a/public/app/core/services/bridge_srv.ts +++ b/public/app/core/services/bridge_srv.ts @@ -1,8 +1,10 @@ import coreModule from 'app/core/core_module'; import appEvents from 'app/core/app_events'; import { store } from 'app/stores/store'; +import { store as reduxStore } from 'app/stores/configureStore'; import { reaction } from 'mobx'; import locationUtil from 'app/core/utils/location_util'; +import { updateLocation } from 'app/core/actions'; // Services that handles angular -> mobx store sync & other react <-> angular sync export class BridgeSrv { @@ -19,12 +21,30 @@ export class BridgeSrv { if (store.view.currentUrl !== angularUrl) { store.view.updatePathAndQuery(this.$location.path(), this.$location.search(), this.$route.current.params); } + const state = reduxStore.getState(); + if (state.location.url !== angularUrl) { + reduxStore.dispatch( + updateLocation({ + path: this.$location.path(), + query: this.$location.search(), + routeParams: this.$route.current.params, + }) + ); + } }); this.$rootScope.$on('$routeChangeSuccess', (evt, data) => { store.view.updatePathAndQuery(this.$location.path(), this.$location.search(), this.$route.current.params); + reduxStore.dispatch( + updateLocation({ + path: this.$location.path(), + query: this.$location.search(), + routeParams: this.$route.current.params, + }) + ); }); + // listen for mobx store changes and update angular reaction( () => store.view.currentUrl, currentUrl => { @@ -39,6 +59,19 @@ export class BridgeSrv { } ); + // Listen for changes in redux location -> update angular location + reduxStore.subscribe(() => { + const state = reduxStore.getState(); + const angularUrl = this.$location.url(); + const url = locationUtil.stripBaseFromUrl(state.location.url); + if (angularUrl !== url) { + this.$timeout(() => { + this.$location.url(url); + }); + console.log('store updating angular $location.url', url); + } + }); + appEvents.on('location-change', payload => { const urlWithoutBase = locationUtil.stripBaseFromUrl(payload.href); if (this.fullPageReloadRoutes.indexOf(urlWithoutBase) > -1) { diff --git a/public/app/features/alerting/containers/AlertRuleList.tsx b/public/app/features/alerting/containers/AlertRuleList.tsx index 665b1508e3e..3c64f490db4 100644 --- a/public/app/features/alerting/containers/AlertRuleList.tsx +++ b/public/app/features/alerting/containers/AlertRuleList.tsx @@ -5,11 +5,13 @@ import classNames from 'classnames'; import PageHeader from 'app/core/components/PageHeader/PageHeader'; import appEvents from 'app/core/app_events'; import Highlighter from 'react-highlight-words'; -import { initNav } from 'app/core/actions'; +import { initNav, updateLocation } from 'app/core/actions'; import { ContainerProps } from 'app/types'; import { getAlertRules, AlertRule } from '../apis'; -interface Props extends ContainerProps {} +interface Props extends ContainerProps { + updateLocation: typeof updateLocation; +} interface State { rules: AlertRule[]; @@ -44,7 +46,9 @@ export class AlertRuleList extends PureComponent { } onStateFilterChanged = evt => { - // this.props.view.updateQuery({ state: evt.target.value }); + this.props.updateLocation({ + query: { state: evt.target.value }, + }); // this.fetchRules(); }; @@ -113,9 +117,7 @@ export class AlertRuleList extends PureComponent {
      - {rules.map(rule => ( - - ))} + {rules.map(rule => )}
    @@ -204,6 +206,7 @@ const mapStateToProps = state => ({ const mapDispatchToProps = { initNav, + updateLocation, }; export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(AlertRuleList)); diff --git a/public/app/types/index.ts b/public/app/types/index.ts index 43d921e3964..9cb5ee85c04 100644 --- a/public/app/types/index.ts +++ b/public/app/types/index.ts @@ -1,4 +1,5 @@ import { NavModel, NavModelItem } from './navModel'; import { ContainerProps } from './container'; +import { LocationState, LocationUpdate, UrlQueryMap, UrlQueryValue } from './location'; -export { NavModel, NavModelItem, ContainerProps }; +export { NavModel, NavModelItem, ContainerProps, LocationState, LocationUpdate, UrlQueryValue, UrlQueryMap }; diff --git a/public/app/types/location.ts b/public/app/types/location.ts new file mode 100644 index 00000000000..4a7f51523a7 --- /dev/null +++ b/public/app/types/location.ts @@ -0,0 +1,15 @@ +export interface LocationUpdate { + path?: string; + query?: UrlQueryMap; + routeParams?: UrlQueryMap; +} + +export interface LocationState { + url: string; + path: string; + query: UrlQueryMap; + routeParams: UrlQueryMap; +} + +export type UrlQueryValue = string | number | boolean | string[] | number[] | boolean[]; +export type UrlQueryMap = { [s: string]: UrlQueryValue }; From 2ac202b22f4d2c6f6e076b851f34be155f64c8e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Sun, 2 Sep 2018 07:11:21 -0700 Subject: [PATCH 581/786] moving things around --- public/app/core/reducers/navModel.ts | 2 -- .../admin/containers/ServerStats.test.tsx | 22 +++++--------- .../features/admin/containers/ServerStats.tsx | 4 +-- .../{containers => }/AlertRuleList.test.tsx | 0 .../{containers => }/AlertRuleList.tsx | 2 +- .../{alert_tab_ctrl.ts => AlertTabCtrl.ts} | 4 +-- ..._edit_ctrl.ts => NotificationsEditCtrl.ts} | 0 ..._list_ctrl.ts => NotificationsListCtrl.ts} | 0 .../__snapshots__/AlertRuleList.test.tsx.snap | 0 public/app/features/alerting/all.ts | 2 -- .../ThresholdMapper.test.ts} | 2 +- .../ThresholdMapper.ts} | 0 .../{alert_def.ts => state/alertDef.ts} | 0 .../alerting/{apis/index.ts => state/apis.ts} | 2 +- public/app/features/all.ts | 3 +- .../annotations/annotation_tooltip.ts | 2 +- public/app/plugins/panel/alertlist/module.ts | 2 +- public/app/plugins/sdk.ts | 2 +- public/app/routes/routes.ts | 2 +- public/app/stores/AlertListStore/helpers.ts | 2 +- public/app/types/container.ts | 3 +- public/app/types/navModel.ts | 2 +- public/test/mocks/common.ts | 29 +++++++++++++++++++ 23 files changed, 53 insertions(+), 34 deletions(-) rename public/app/features/alerting/{containers => }/AlertRuleList.test.tsx (100%) rename public/app/features/alerting/{containers => }/AlertRuleList.tsx (99%) rename public/app/features/alerting/{alert_tab_ctrl.ts => AlertTabCtrl.ts} (99%) rename public/app/features/alerting/{notification_edit_ctrl.ts => NotificationsEditCtrl.ts} (100%) rename public/app/features/alerting/{notifications_list_ctrl.ts => NotificationsListCtrl.ts} (100%) rename public/app/features/alerting/{containers => }/__snapshots__/AlertRuleList.test.tsx.snap (100%) delete mode 100644 public/app/features/alerting/all.ts rename public/app/features/alerting/{specs/threshold_mapper.test.ts => state/ThresholdMapper.test.ts} (97%) rename public/app/features/alerting/{threshold_mapper.ts => state/ThresholdMapper.ts} (100%) rename public/app/features/alerting/{alert_def.ts => state/alertDef.ts} (100%) rename public/app/features/alerting/{apis/index.ts => state/apis.ts} (97%) diff --git a/public/app/core/reducers/navModel.ts b/public/app/core/reducers/navModel.ts index c00441c4881..4e9a7f8e434 100644 --- a/public/app/core/reducers/navModel.ts +++ b/public/app/core/reducers/navModel.ts @@ -12,7 +12,6 @@ function getNotFoundModel(): NavModel { }; return { - breadcrumbs: [node], node: node, main: node, }; @@ -53,7 +52,6 @@ const navModelReducer = (state = initialState, action: Action): NavModel => { return { main: main, node: node, - breadcrumbs: [], }; } } diff --git a/public/app/features/admin/containers/ServerStats.test.tsx b/public/app/features/admin/containers/ServerStats.test.tsx index a329a47527d..a89e78cb4ba 100644 --- a/public/app/features/admin/containers/ServerStats.test.tsx +++ b/public/app/features/admin/containers/ServerStats.test.tsx @@ -1,26 +1,18 @@ import React from 'react'; import renderer from 'react-test-renderer'; import { ServerStats } from './ServerStats'; -import { RootStore } from 'app/stores/RootStore/RootStore'; -import { backendSrv, createNavTree } from 'test/mocks/common'; +import { initNav } from 'test/mocks/common'; +import { ServerStat } from '../apis'; describe('ServerStats', () => { it('Should render table with stats', done => { - backendSrv.get.mockReturnValue( - Promise.resolve({ - dashboards: 10, - }) - ); + const stats: ServerStat[] = [{ name: 'test', value: 'asd' }]; - const store = RootStore.create( - {}, - { - backendSrv: backendSrv, - navTree: createNavTree('cfg', 'admin', 'server-stats'), - } - ); + let getServerStats = () => { + return Promise.resolve(stats); + }; - const page = renderer.create(); + const page = renderer.create(); setTimeout(() => { expect(page.toJSON()).toMatchSnapshot(); diff --git a/public/app/features/admin/containers/ServerStats.tsx b/public/app/features/admin/containers/ServerStats.tsx index 7e96dcf4e0e..29611696efa 100644 --- a/public/app/features/admin/containers/ServerStats.tsx +++ b/public/app/features/admin/containers/ServerStats.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { PureComponent } from 'react'; import { hot } from 'react-hot-loader'; import { connect } from 'react-redux'; import { initNav } from 'app/core/actions'; @@ -14,7 +14,7 @@ interface State { stats: ServerStat[]; } -export class ServerStats extends React.Component { +export class ServerStats extends PureComponent { constructor(props) { super(props); diff --git a/public/app/features/alerting/containers/AlertRuleList.test.tsx b/public/app/features/alerting/AlertRuleList.test.tsx similarity index 100% rename from public/app/features/alerting/containers/AlertRuleList.test.tsx rename to public/app/features/alerting/AlertRuleList.test.tsx diff --git a/public/app/features/alerting/containers/AlertRuleList.tsx b/public/app/features/alerting/AlertRuleList.tsx similarity index 99% rename from public/app/features/alerting/containers/AlertRuleList.tsx rename to public/app/features/alerting/AlertRuleList.tsx index 3c64f490db4..e2e6d1a719a 100644 --- a/public/app/features/alerting/containers/AlertRuleList.tsx +++ b/public/app/features/alerting/AlertRuleList.tsx @@ -7,7 +7,7 @@ import appEvents from 'app/core/app_events'; import Highlighter from 'react-highlight-words'; import { initNav, updateLocation } from 'app/core/actions'; import { ContainerProps } from 'app/types'; -import { getAlertRules, AlertRule } from '../apis'; +import { getAlertRules, AlertRule } from './state/apis'; interface Props extends ContainerProps { updateLocation: typeof updateLocation; diff --git a/public/app/features/alerting/alert_tab_ctrl.ts b/public/app/features/alerting/AlertTabCtrl.ts similarity index 99% rename from public/app/features/alerting/alert_tab_ctrl.ts rename to public/app/features/alerting/AlertTabCtrl.ts index a25d37913d4..040b293b244 100644 --- a/public/app/features/alerting/alert_tab_ctrl.ts +++ b/public/app/features/alerting/AlertTabCtrl.ts @@ -1,7 +1,7 @@ import _ from 'lodash'; -import { ThresholdMapper } from './threshold_mapper'; +import { ThresholdMapper } from './state/ThresholdMapper'; import { QueryPart } from 'app/core/components/query_part/query_part'; -import alertDef from './alert_def'; +import alertDef from './state/alertDef'; import config from 'app/core/config'; import appEvents from 'app/core/app_events'; diff --git a/public/app/features/alerting/notification_edit_ctrl.ts b/public/app/features/alerting/NotificationsEditCtrl.ts similarity index 100% rename from public/app/features/alerting/notification_edit_ctrl.ts rename to public/app/features/alerting/NotificationsEditCtrl.ts diff --git a/public/app/features/alerting/notifications_list_ctrl.ts b/public/app/features/alerting/NotificationsListCtrl.ts similarity index 100% rename from public/app/features/alerting/notifications_list_ctrl.ts rename to public/app/features/alerting/NotificationsListCtrl.ts diff --git a/public/app/features/alerting/containers/__snapshots__/AlertRuleList.test.tsx.snap b/public/app/features/alerting/__snapshots__/AlertRuleList.test.tsx.snap similarity index 100% rename from public/app/features/alerting/containers/__snapshots__/AlertRuleList.test.tsx.snap rename to public/app/features/alerting/__snapshots__/AlertRuleList.test.tsx.snap diff --git a/public/app/features/alerting/all.ts b/public/app/features/alerting/all.ts deleted file mode 100644 index 91d3a4109e7..00000000000 --- a/public/app/features/alerting/all.ts +++ /dev/null @@ -1,2 +0,0 @@ -import './notifications_list_ctrl'; -import './notification_edit_ctrl'; diff --git a/public/app/features/alerting/specs/threshold_mapper.test.ts b/public/app/features/alerting/state/ThresholdMapper.test.ts similarity index 97% rename from public/app/features/alerting/specs/threshold_mapper.test.ts rename to public/app/features/alerting/state/ThresholdMapper.test.ts index 922d9c8787e..d8ab54234cd 100644 --- a/public/app/features/alerting/specs/threshold_mapper.test.ts +++ b/public/app/features/alerting/state/ThresholdMapper.test.ts @@ -1,6 +1,6 @@ import { describe, it, expect } from 'test/lib/common'; -import { ThresholdMapper } from '../threshold_mapper'; +import { ThresholdMapper } from './threshold_mapper'; describe('ThresholdMapper', () => { describe('with greater than evaluator', () => { diff --git a/public/app/features/alerting/threshold_mapper.ts b/public/app/features/alerting/state/ThresholdMapper.ts similarity index 100% rename from public/app/features/alerting/threshold_mapper.ts rename to public/app/features/alerting/state/ThresholdMapper.ts diff --git a/public/app/features/alerting/alert_def.ts b/public/app/features/alerting/state/alertDef.ts similarity index 100% rename from public/app/features/alerting/alert_def.ts rename to public/app/features/alerting/state/alertDef.ts diff --git a/public/app/features/alerting/apis/index.ts b/public/app/features/alerting/state/apis.ts similarity index 97% rename from public/app/features/alerting/apis/index.ts rename to public/app/features/alerting/state/apis.ts index ebfdbd34024..44cadc05215 100644 --- a/public/app/features/alerting/apis/index.ts +++ b/public/app/features/alerting/state/apis.ts @@ -1,5 +1,5 @@ import { getBackendSrv } from 'app/core/services/backend_srv'; -import alertDef from '../alert_def'; +import alertDef from './alertDef'; import moment from 'moment'; export interface AlertRule { diff --git a/public/app/features/all.ts b/public/app/features/all.ts index df987a8b59b..065f399cae3 100644 --- a/public/app/features/all.ts +++ b/public/app/features/all.ts @@ -9,5 +9,6 @@ import './snapshot/all'; import './panel/all'; import './org/all'; import './admin/admin'; -import './alerting/all'; +import './alerting/NotificationsEditCtrl'; +import './alerting/NotificationsListCtrl'; import './styleguide/styleguide'; diff --git a/public/app/features/annotations/annotation_tooltip.ts b/public/app/features/annotations/annotation_tooltip.ts index ed2d797b7bf..0cb0c6a9419 100644 --- a/public/app/features/annotations/annotation_tooltip.ts +++ b/public/app/features/annotations/annotation_tooltip.ts @@ -1,7 +1,7 @@ import _ from 'lodash'; import $ from 'jquery'; import coreModule from 'app/core/core_module'; -import alertDef from '../alerting/alert_def'; +import alertDef from '../alerting/state/alertDef'; /** @ngInject **/ export function annotationTooltipDirective($sanitize, dashboardSrv, contextSrv, $compile) { diff --git a/public/app/plugins/panel/alertlist/module.ts b/public/app/plugins/panel/alertlist/module.ts index b171f590e94..f5a23f4748b 100644 --- a/public/app/plugins/panel/alertlist/module.ts +++ b/public/app/plugins/panel/alertlist/module.ts @@ -1,6 +1,6 @@ import _ from 'lodash'; import moment from 'moment'; -import alertDef from '../../../features/alerting/alert_def'; +import alertDef from '../../../features/alerting/state/alertDef'; import { PanelCtrl } from 'app/plugins/sdk'; import * as dateMath from 'app/core/utils/datemath'; diff --git a/public/app/plugins/sdk.ts b/public/app/plugins/sdk.ts index 2734426bd19..0f183271495 100644 --- a/public/app/plugins/sdk.ts +++ b/public/app/plugins/sdk.ts @@ -1,7 +1,7 @@ import { PanelCtrl } from 'app/features/panel/panel_ctrl'; import { MetricsPanelCtrl } from 'app/features/panel/metrics_panel_ctrl'; import { QueryCtrl } from 'app/features/panel/query_ctrl'; -import { alertTab } from 'app/features/alerting/alert_tab_ctrl'; +import { alertTab } from 'app/features/alerting/AlertTabCtrl'; import { loadPluginCss } from 'app/features/plugins/plugin_loader'; export { PanelCtrl, MetricsPanelCtrl, QueryCtrl, alertTab, loadPluginCss }; diff --git a/public/app/routes/routes.ts b/public/app/routes/routes.ts index 7b1e223afe5..dfd215f7056 100644 --- a/public/app/routes/routes.ts +++ b/public/app/routes/routes.ts @@ -2,7 +2,7 @@ import './dashboard_loaders'; import './ReactContainer'; import ServerStats from 'app/features/admin/containers/ServerStats'; -import AlertRuleList from 'app/features/alerting/containers/AlertRuleList'; +import AlertRuleList from 'app/features/alerting/AlertRuleList'; import FolderSettings from 'app/containers/ManageDashboards/FolderSettings'; import FolderPermissions from 'app/containers/ManageDashboards/FolderPermissions'; import TeamPages from 'app/containers/Teams/TeamPages'; diff --git a/public/app/stores/AlertListStore/helpers.ts b/public/app/stores/AlertListStore/helpers.ts index c460a697967..4d1ddcc30e0 100644 --- a/public/app/stores/AlertListStore/helpers.ts +++ b/public/app/stores/AlertListStore/helpers.ts @@ -1,5 +1,5 @@ import moment from 'moment'; -import alertDef from 'app/features/alerting/alert_def'; +import alertDef from 'app/features/alerting/state/alertDef'; export function setStateFields(rule, state) { const stateModel = alertDef.getStateDisplayModel(state); diff --git a/public/app/types/container.ts b/public/app/types/container.ts index 174bc0c8460..98b5248fdd6 100644 --- a/public/app/types/container.ts +++ b/public/app/types/container.ts @@ -1,6 +1,7 @@ import { NavModel } from './navModel'; +import { initNav } from 'app/core/actions'; export interface ContainerProps { navModel: NavModel; - initNav: (...args: string[]) => void; + initNav: typeof initNav; } diff --git a/public/app/types/navModel.ts b/public/app/types/navModel.ts index e1a4265847c..9464858f967 100644 --- a/public/app/types/navModel.ts +++ b/public/app/types/navModel.ts @@ -9,11 +9,11 @@ export interface NavModelItem { hideFromTabs?: boolean; divider?: boolean; children?: NavModelItem[]; + breadcrumbs?: NavModelItem[]; target?: string; } export interface NavModel { - breadcrumbs: NavModelItem[]; main: NavModelItem; node: NavModelItem; } diff --git a/public/test/mocks/common.ts b/public/test/mocks/common.ts index 64d12fdf725..5350636573d 100644 --- a/public/test/mocks/common.ts +++ b/public/test/mocks/common.ts @@ -1,3 +1,5 @@ +import { NavModel, NavModelItem } from 'app/types'; + export const backendSrv = { get: jest.fn(), getDashboard: jest.fn(), @@ -17,3 +19,30 @@ export function createNavTree(...args) { return root; } + +export function getNavModel(title: string, tabs: string[]): NavModel { + const node: NavModelItem = { + id: title, + text: title, + icon: 'fa fa-fw fa-warning', + subTitle: 'subTitle', + url: title, + children: [], + breadcrumbs: [], + }; + + for (let tab of tabs) { + node.children.push({ + id: tab, + icon: 'icon', + subTitle: 'subTitle', + url: title, + text: title, + }); + } + + return { + node: node, + main: node, + }; +} From 7b06800295189343bb61e6ef56cebe70056cc23b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Sun, 2 Sep 2018 10:36:36 -0700 Subject: [PATCH 582/786] refactor: changed nav store to use nav index and selector instead of initNav action --- public/app/core/actions/index.ts | 3 +- public/app/core/actions/navModel.ts | 16 ++-- public/app/core/reducers/index.ts | 4 +- public/app/core/reducers/navModel.ts | 69 ++++----------- public/app/core/selectors/navModel.ts | 39 +++++++++ public/app/features/admin/apis/index.ts | 2 +- .../admin/containers/ServerStats.test.tsx | 7 +- .../features/admin/containers/ServerStats.tsx | 19 ++-- .../__snapshots__/ServerStats.test.tsx.snap | 87 ++++--------------- .../app/features/alerting/AlertRuleList.tsx | 15 ++-- .../alerting/state/ThresholdMapper.test.ts | 2 +- public/app/types/container.ts | 7 -- public/app/types/index.ts | 46 +++++++++- public/app/types/location.ts | 15 ---- public/app/types/navModel.ts | 19 ---- public/test/jest-setup.ts | 21 +++++ public/test/mocks/common.ts | 5 +- 17 files changed, 174 insertions(+), 202 deletions(-) create mode 100644 public/app/core/selectors/navModel.ts delete mode 100644 public/app/types/container.ts delete mode 100644 public/app/types/location.ts delete mode 100644 public/app/types/navModel.ts diff --git a/public/app/core/actions/index.ts b/public/app/core/actions/index.ts index 7a965f82dd1..74b61f845c0 100644 --- a/public/app/core/actions/index.ts +++ b/public/app/core/actions/index.ts @@ -1,4 +1,3 @@ -import { initNav } from './navModel'; import { updateLocation } from './location'; -export { initNav, updateLocation }; +export { updateLocation }; diff --git a/public/app/core/actions/navModel.ts b/public/app/core/actions/navModel.ts index 048afd4f8ff..56d129fd263 100644 --- a/public/app/core/actions/navModel.ts +++ b/public/app/core/actions/navModel.ts @@ -1,11 +1,13 @@ -export type Action = InitNavModelAction; +export type Action = UpdateNavIndexAction; -export interface InitNavModelAction { - type: 'INIT_NAV_MODEL'; - args: string[]; +// this action is not used yet +// kind of just a placeholder, will be need for dynamic pages +// like datasource edit, teams edit page + +export interface UpdateNavIndexAction { + type: 'UPDATE_NAV_INDEX'; } -export const initNav = (...args: string[]): InitNavModelAction => ({ - type: 'INIT_NAV_MODEL', - args: args, +export const updateNavIndex = (): UpdateNavIndexAction => ({ + type: 'UPDATE_NAV_INDEX', }); diff --git a/public/app/core/reducers/index.ts b/public/app/core/reducers/index.ts index 98f796981e4..a3f9ca909c9 100644 --- a/public/app/core/reducers/index.ts +++ b/public/app/core/reducers/index.ts @@ -1,7 +1,7 @@ -import navModel from './navModel'; +import { navIndexReducer as navIndex } from './navModel'; import location from './location'; export default { - navModel, + navIndex, location, }; diff --git a/public/app/core/reducers/navModel.ts b/public/app/core/reducers/navModel.ts index 4e9a7f8e434..26acdb39a3d 100644 --- a/public/app/core/reducers/navModel.ts +++ b/public/app/core/reducers/navModel.ts @@ -1,62 +1,29 @@ import { Action } from 'app/core/actions/navModel'; -import { NavModel, NavModelItem } from 'app/types'; +import { NavModelItem, NavIndex } from 'app/types'; import config from 'app/core/config'; -function getNotFoundModel(): NavModel { - var node: NavModelItem = { - id: 'not-found', - text: 'Page not found', - icon: 'fa fa-fw fa-warning', - subTitle: '404 Error', - url: 'not-found', - }; - - return { - node: node, - main: node, - }; +export function buildInitialState(): NavIndex { + const navIndex: NavIndex = {}; + const rootNodes = config.bootData.navTree as NavModelItem[]; + buildNavIndex(navIndex, rootNodes); + return navIndex; } -export const initialState: NavModel = getNotFoundModel(); +function buildNavIndex(navIndex: NavIndex, children: NavModelItem[], parentItem?: NavModelItem) { + for (const node of children) { + navIndex[node.id] = { + ...node, + parentItem: parentItem, + }; -const navModelReducer = (state = initialState, action: Action): NavModel => { - switch (action.type) { - case 'INIT_NAV_MODEL': { - let children = config.bootData.navTree as NavModelItem[]; - let main, node; - const parents = []; - - for (const id of action.args) { - node = children.find(el => el.id === id); - - if (!node) { - throw new Error(`NavItem with id ${id} not found`); - } - - children = node.children; - parents.push(node); - } - - main = parents[parents.length - 2]; - - if (main.children) { - for (const item of main.children) { - item.active = false; - - if (item.url === node.url) { - item.active = true; - } - } - } - - return { - main: main, - node: node, - }; + if (node.children) { + buildNavIndex(navIndex, node.children, node); } } +} +export const initialState: NavIndex = buildInitialState(); + +export const navIndexReducer = (state = initialState, action: Action): NavIndex => { return state; }; - -export default navModelReducer; diff --git a/public/app/core/selectors/navModel.ts b/public/app/core/selectors/navModel.ts new file mode 100644 index 00000000000..5f2d0318dff --- /dev/null +++ b/public/app/core/selectors/navModel.ts @@ -0,0 +1,39 @@ +import { NavModel, NavModelItem, NavIndex } from 'app/types'; + +function getNotFoundModel(): NavModel { + var node: NavModelItem = { + id: 'not-found', + text: 'Page not found', + icon: 'fa fa-fw fa-warning', + subTitle: '404 Error', + url: 'not-found', + }; + + return { + node: node, + main: node, + }; +} + +export function selectNavNode(navIndex: NavIndex, id: string): NavModel { + if (navIndex[id]) { + const node = navIndex[id]; + const main = { + ...node.parentItem, + }; + + main.children = main.children.map(item => { + return { + ...item, + active: item.url === node.url, + }; + }); + + return { + node: node, + main: main, + }; + } else { + return getNotFoundModel(); + } +} diff --git a/public/app/features/admin/apis/index.ts b/public/app/features/admin/apis/index.ts index 888cfd4f58f..d81fd299493 100644 --- a/public/app/features/admin/apis/index.ts +++ b/public/app/features/admin/apis/index.ts @@ -2,7 +2,7 @@ import { getBackendSrv } from 'app/core/services/backend_srv'; export interface ServerStat { name: string; - value: string; + value: number; } export const getServerStats = async (): Promise => { diff --git a/public/app/features/admin/containers/ServerStats.test.tsx b/public/app/features/admin/containers/ServerStats.test.tsx index a89e78cb4ba..e12dfc3bed4 100644 --- a/public/app/features/admin/containers/ServerStats.test.tsx +++ b/public/app/features/admin/containers/ServerStats.test.tsx @@ -1,18 +1,19 @@ import React from 'react'; import renderer from 'react-test-renderer'; import { ServerStats } from './ServerStats'; -import { initNav } from 'test/mocks/common'; +import { createNavModel } from 'test/mocks/common'; import { ServerStat } from '../apis'; describe('ServerStats', () => { it('Should render table with stats', done => { - const stats: ServerStat[] = [{ name: 'test', value: 'asd' }]; + const navModel = createNavModel('Admin', 'stats'); + const stats: ServerStat[] = [{ name: 'Total dashboards', value: 10 }, { name: 'Total Users', value: 1 }]; let getServerStats = () => { return Promise.resolve(stats); }; - const page = renderer.create(); + const page = renderer.create(); setTimeout(() => { expect(page.toJSON()).toMatchSnapshot(); diff --git a/public/app/features/admin/containers/ServerStats.tsx b/public/app/features/admin/containers/ServerStats.tsx index 29611696efa..0b44a9af65e 100644 --- a/public/app/features/admin/containers/ServerStats.tsx +++ b/public/app/features/admin/containers/ServerStats.tsx @@ -1,12 +1,13 @@ import React, { PureComponent } from 'react'; import { hot } from 'react-hot-loader'; import { connect } from 'react-redux'; -import { initNav } from 'app/core/actions'; -import { ContainerProps } from 'app/types'; +import { NavModel, StoreState } from 'app/types'; +import { selectNavNode } from 'app/core/selectors/navModel'; import { getServerStats, ServerStat } from '../apis'; import PageHeader from 'app/core/components/PageHeader/PageHeader'; -interface Props extends ContainerProps { +interface Props { + navModel: NavModel; getServerStats: () => Promise; } @@ -21,8 +22,6 @@ export class ServerStats extends PureComponent { this.state = { stats: [], }; - - this.props.initNav('cfg', 'admin', 'server-stats'); } async componentDidMount() { @@ -66,13 +65,9 @@ function StatItem(stat: ServerStat) { ); } -const mapStateToProps = state => ({ - navModel: state.navModel, +const mapStateToProps = (state: StoreState) => ({ + navModel: selectNavNode(state.navIndex, 'server-stats'), getServerStats: getServerStats, }); -const mapDispatchToProps = { - initNav, -}; - -export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(ServerStats)); +export default hot(module)(connect(mapStateToProps)(ServerStats)); diff --git a/public/app/features/admin/containers/__snapshots__/ServerStats.test.tsx.snap b/public/app/features/admin/containers/__snapshots__/ServerStats.test.tsx.snap index eac793ca2ca..63de5bcd870 100644 --- a/public/app/features/admin/containers/__snapshots__/ServerStats.test.tsx.snap +++ b/public/app/features/admin/containers/__snapshots__/ServerStats.test.tsx.snap @@ -17,8 +17,9 @@ exports[`ServerStats Should render table with stats 1`] = ` - - +
    - admin-Text + Admin - +
    + subTitle +
    @@ -60,13 +65,13 @@ exports[`ServerStats Should render table with stats 1`] = ` > - server-stats-Text + Admin @@ -101,66 +106,10 @@ exports[`ServerStats Should render table with stats 1`] = ` - Total users + Total Users - 0 - - - - - Active users (seen last 30 days) - - - 0 - - - - - Total orgs - - - 0 - - - - - Total playlists - - - 0 - - - - - Total snapshots - - - 0 - - - - - Total dashboard tags - - - 0 - - - - - Total starred dashboards - - - 0 - - - - - Total alerts - - - 0 + 1 diff --git a/public/app/features/alerting/AlertRuleList.tsx b/public/app/features/alerting/AlertRuleList.tsx index e2e6d1a719a..84994555445 100644 --- a/public/app/features/alerting/AlertRuleList.tsx +++ b/public/app/features/alerting/AlertRuleList.tsx @@ -5,11 +5,13 @@ import classNames from 'classnames'; import PageHeader from 'app/core/components/PageHeader/PageHeader'; import appEvents from 'app/core/app_events'; import Highlighter from 'react-highlight-words'; -import { initNav, updateLocation } from 'app/core/actions'; -import { ContainerProps } from 'app/types'; +import { updateLocation } from 'app/core/actions'; +import { selectNavNode } from 'app/core/selectors/navModel'; +import { NavModel, StoreState } from 'app/types'; import { getAlertRules, AlertRule } from './state/apis'; -interface Props extends ContainerProps { +interface Props { + navModel: NavModel; updateLocation: typeof updateLocation; } @@ -37,8 +39,6 @@ export class AlertRuleList extends PureComponent { search: '', stateFilter: '', }; - - this.props.initNav('alerting', 'alert-list'); } componentDidMount() { @@ -200,12 +200,11 @@ export class AlertRuleItem extends React.Component { } } -const mapStateToProps = state => ({ - navModel: state.navModel, +const mapStateToProps = (state: StoreState) => ({ + navModel: selectNavNode(state.navIndex, 'alert-list'), }); const mapDispatchToProps = { - initNav, updateLocation, }; diff --git a/public/app/features/alerting/state/ThresholdMapper.test.ts b/public/app/features/alerting/state/ThresholdMapper.test.ts index d8ab54234cd..8e91d0b6d0a 100644 --- a/public/app/features/alerting/state/ThresholdMapper.test.ts +++ b/public/app/features/alerting/state/ThresholdMapper.test.ts @@ -1,6 +1,6 @@ import { describe, it, expect } from 'test/lib/common'; -import { ThresholdMapper } from './threshold_mapper'; +import { ThresholdMapper } from './ThresholdMapper'; describe('ThresholdMapper', () => { describe('with greater than evaluator', () => { diff --git a/public/app/types/container.ts b/public/app/types/container.ts deleted file mode 100644 index 98b5248fdd6..00000000000 --- a/public/app/types/container.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { NavModel } from './navModel'; -import { initNav } from 'app/core/actions'; - -export interface ContainerProps { - navModel: NavModel; - initNav: typeof initNav; -} diff --git a/public/app/types/index.ts b/public/app/types/index.ts index 9cb5ee85c04..930c08c9eb0 100644 --- a/public/app/types/index.ts +++ b/public/app/types/index.ts @@ -1,5 +1,43 @@ -import { NavModel, NavModelItem } from './navModel'; -import { ContainerProps } from './container'; -import { LocationState, LocationUpdate, UrlQueryMap, UrlQueryValue } from './location'; +export interface LocationUpdate { + path?: string; + query?: UrlQueryMap; + routeParams?: UrlQueryMap; +} -export { NavModel, NavModelItem, ContainerProps, LocationState, LocationUpdate, UrlQueryValue, UrlQueryMap }; +export interface LocationState { + url: string; + path: string; + query: UrlQueryMap; + routeParams: UrlQueryMap; +} + +export type UrlQueryValue = string | number | boolean | string[] | number[] | boolean[]; +export type UrlQueryMap = { [s: string]: UrlQueryValue }; + +export interface NavModelItem { + text: string; + url: string; + subTitle?: string; + icon?: string; + img?: string; + id: string; + active?: boolean; + hideFromTabs?: boolean; + divider?: boolean; + children?: NavModelItem[]; + breadcrumbs?: NavModelItem[]; + target?: string; + parentItem?: NavModelItem; +} + +export interface NavModel { + main: NavModelItem; + node: NavModelItem; +} + +export type NavIndex = { [s: string]: NavModelItem }; + +export interface StoreState { + navIndex: NavIndex; + location: LocationState; +} diff --git a/public/app/types/location.ts b/public/app/types/location.ts deleted file mode 100644 index 4a7f51523a7..00000000000 --- a/public/app/types/location.ts +++ /dev/null @@ -1,15 +0,0 @@ -export interface LocationUpdate { - path?: string; - query?: UrlQueryMap; - routeParams?: UrlQueryMap; -} - -export interface LocationState { - url: string; - path: string; - query: UrlQueryMap; - routeParams: UrlQueryMap; -} - -export type UrlQueryValue = string | number | boolean | string[] | number[] | boolean[]; -export type UrlQueryMap = { [s: string]: UrlQueryValue }; diff --git a/public/app/types/navModel.ts b/public/app/types/navModel.ts deleted file mode 100644 index 9464858f967..00000000000 --- a/public/app/types/navModel.ts +++ /dev/null @@ -1,19 +0,0 @@ -export interface NavModelItem { - text: string; - url: string; - subTitle?: string; - icon?: string; - img?: string; - id: string; - active?: boolean; - hideFromTabs?: boolean; - divider?: boolean; - children?: NavModelItem[]; - breadcrumbs?: NavModelItem[]; - target?: string; -} - -export interface NavModel { - main: NavModelItem; - node: NavModelItem; -} diff --git a/public/test/jest-setup.ts b/public/test/jest-setup.ts index fed65097ac7..7b326a279b7 100644 --- a/public/test/jest-setup.ts +++ b/public/test/jest-setup.ts @@ -20,3 +20,24 @@ configure({ adapter: new Adapter() }); const global = window; global.$ = global.jQuery = $; + +const localStorageMock = (function() { + var store = {}; + return { + getItem: function(key) { + return store[key]; + }, + setItem: function(key, value) { + store[key] = value.toString(); + }, + clear: function() { + store = {}; + }, + removeItem: function(key) { + delete store[key]; + }, + }; +})(); + +global.localStorage = localStorageMock; +// Object.defineProperty(window, 'localStorage', { value: localStorageMock }); diff --git a/public/test/mocks/common.ts b/public/test/mocks/common.ts index 5350636573d..1c7bdb4f1e2 100644 --- a/public/test/mocks/common.ts +++ b/public/test/mocks/common.ts @@ -20,7 +20,7 @@ export function createNavTree(...args) { return root; } -export function getNavModel(title: string, tabs: string[]): NavModel { +export function createNavModel(title: string, ...tabs: string[]): NavModel { const node: NavModelItem = { id: title, text: title, @@ -38,9 +38,12 @@ export function getNavModel(title: string, tabs: string[]): NavModel { subTitle: 'subTitle', url: title, text: title, + active: false, }); } + node.children[0].active = true; + return { node: node, main: node, From 2a64d19f5b22ca44f6c3d5ecffec75f62b1fba7d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Sun, 2 Sep 2018 11:36:03 -0700 Subject: [PATCH 583/786] wip: load alert rules via redux --- public/app/core/reducers/index.ts | 2 +- public/app/core/reducers/location.ts | 4 +- public/app/core/selectors/navModel.ts | 2 +- .../features/admin/containers/ServerStats.tsx | 4 +- .../app/features/alerting/AlertRuleList.tsx | 27 +++++----- public/app/features/alerting/state/actions.ts | 26 ++++++++++ public/app/features/alerting/state/apis.ts | 52 ------------------- .../app/features/alerting/state/reducers.ts | 46 ++++++++++++++++ public/app/stores/configureStore.ts | 4 +- public/app/types/index.ts | 33 ++++++++++++ 10 files changed, 126 insertions(+), 74 deletions(-) create mode 100644 public/app/features/alerting/state/actions.ts delete mode 100644 public/app/features/alerting/state/apis.ts create mode 100644 public/app/features/alerting/state/reducers.ts diff --git a/public/app/core/reducers/index.ts b/public/app/core/reducers/index.ts index a3f9ca909c9..be13528c91c 100644 --- a/public/app/core/reducers/index.ts +++ b/public/app/core/reducers/index.ts @@ -1,5 +1,5 @@ import { navIndexReducer as navIndex } from './navModel'; -import location from './location'; +import { locationReducer as location } from './location'; export default { navIndex, diff --git a/public/app/core/reducers/location.ts b/public/app/core/reducers/location.ts index 5676c82844a..4591448d082 100644 --- a/public/app/core/reducers/location.ts +++ b/public/app/core/reducers/location.ts @@ -16,7 +16,7 @@ function renderUrl(path: string, query: UrlQueryMap): string { return path; } -const routerReducer = (state = initialState, action: Action): LocationState => { +export const locationReducer = (state = initialState, action: Action): LocationState => { switch (action.type) { case 'UPDATE_LOCATION': { const { path, query, routeParams } = action.payload; @@ -31,5 +31,3 @@ const routerReducer = (state = initialState, action: Action): LocationState => { return state; }; - -export default routerReducer; diff --git a/public/app/core/selectors/navModel.ts b/public/app/core/selectors/navModel.ts index 5f2d0318dff..a7e1c3330bd 100644 --- a/public/app/core/selectors/navModel.ts +++ b/public/app/core/selectors/navModel.ts @@ -15,7 +15,7 @@ function getNotFoundModel(): NavModel { }; } -export function selectNavNode(navIndex: NavIndex, id: string): NavModel { +export function getNavModel(navIndex: NavIndex, id: string): NavModel { if (navIndex[id]) { const node = navIndex[id]; const main = { diff --git a/public/app/features/admin/containers/ServerStats.tsx b/public/app/features/admin/containers/ServerStats.tsx index 0b44a9af65e..97419ec9301 100644 --- a/public/app/features/admin/containers/ServerStats.tsx +++ b/public/app/features/admin/containers/ServerStats.tsx @@ -2,7 +2,7 @@ import React, { PureComponent } from 'react'; import { hot } from 'react-hot-loader'; import { connect } from 'react-redux'; import { NavModel, StoreState } from 'app/types'; -import { selectNavNode } from 'app/core/selectors/navModel'; +import { getNavModel } from 'app/core/selectors/navModel'; import { getServerStats, ServerStat } from '../apis'; import PageHeader from 'app/core/components/PageHeader/PageHeader'; @@ -66,7 +66,7 @@ function StatItem(stat: ServerStat) { } const mapStateToProps = (state: StoreState) => ({ - navModel: selectNavNode(state.navIndex, 'server-stats'), + navModel: getNavModel(state.navIndex, 'server-stats'), getServerStats: getServerStats, }); diff --git a/public/app/features/alerting/AlertRuleList.tsx b/public/app/features/alerting/AlertRuleList.tsx index 84994555445..faa46945536 100644 --- a/public/app/features/alerting/AlertRuleList.tsx +++ b/public/app/features/alerting/AlertRuleList.tsx @@ -6,13 +6,15 @@ import PageHeader from 'app/core/components/PageHeader/PageHeader'; import appEvents from 'app/core/app_events'; import Highlighter from 'react-highlight-words'; import { updateLocation } from 'app/core/actions'; -import { selectNavNode } from 'app/core/selectors/navModel'; -import { NavModel, StoreState } from 'app/types'; -import { getAlertRules, AlertRule } from './state/apis'; +import { getNavModel } from 'app/core/selectors/navModel'; +import { NavModel, StoreState, AlertRule } from 'app/types'; +import { getAlertRulesAsync } from './state/actions'; interface Props { navModel: NavModel; + alertRules: AlertRule[]; updateLocation: typeof updateLocation; + getAlertRulesAsync: typeof getAlertRulesAsync; } interface State { @@ -49,16 +51,11 @@ export class AlertRuleList extends PureComponent { this.props.updateLocation({ query: { state: evt.target.value }, }); - // this.fetchRules(); + this.fetchRules(); }; async fetchRules() { - try { - const rules = await getAlertRules(); - this.setState({ rules }); - } catch (error) { - console.error(error); - } + await this.props.getAlertRulesAsync(); // this.props.alertList.loadRules({ // state: this.props.view.query.get('state') || 'all', @@ -78,8 +75,8 @@ export class AlertRuleList extends PureComponent { }; render() { - const { navModel } = this.props; - const { rules, search, stateFilter } = this.state; + const { navModel, alertRules } = this.props; + const { search, stateFilter } = this.state; return (
    @@ -117,7 +114,7 @@ export class AlertRuleList extends PureComponent {
      - {rules.map(rule => )} + {alertRules.map(rule => )}
    @@ -201,11 +198,13 @@ export class AlertRuleItem extends React.Component { } const mapStateToProps = (state: StoreState) => ({ - navModel: selectNavNode(state.navIndex, 'alert-list'), + navModel: getNavModel(state.navIndex, 'alert-list'), + alertRules: state.alertRules, }); const mapDispatchToProps = { updateLocation, + getAlertRulesAsync, }; export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(AlertRuleList)); diff --git a/public/app/features/alerting/state/actions.ts b/public/app/features/alerting/state/actions.ts new file mode 100644 index 00000000000..0f9caa9d47f --- /dev/null +++ b/public/app/features/alerting/state/actions.ts @@ -0,0 +1,26 @@ +import { Dispatch } from 'redux'; +import { getBackendSrv } from 'app/core/services/backend_srv'; +import { AlertRule } from 'app/types'; + +export interface LoadAlertRulesAction { + type: 'LOAD_ALERT_RULES'; + payload: AlertRule[]; +} + +export const loadAlertRules = (rules: AlertRule[]): LoadAlertRulesAction => ({ + type: 'LOAD_ALERT_RULES', + payload: rules, +}); + +export type Action = LoadAlertRulesAction; + +export const getAlertRulesAsync = () => async (dispatch: Dispatch): Promise => { + try { + const rules = await getBackendSrv().get('/api/alerts', {}); + dispatch(loadAlertRules(rules)); + return rules; + } catch (error) { + console.error(error); + throw error; + } +}; diff --git a/public/app/features/alerting/state/apis.ts b/public/app/features/alerting/state/apis.ts deleted file mode 100644 index 44cadc05215..00000000000 --- a/public/app/features/alerting/state/apis.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { getBackendSrv } from 'app/core/services/backend_srv'; -import alertDef from './alertDef'; -import moment from 'moment'; - -export interface AlertRule { - id: number; - dashboardId: number; - panelId: number; - name: string; - state: string; - stateText: string; - stateIcon: string; - stateClass: string; - stateAge: string; - info?: string; - url: string; -} - -export function setStateFields(rule, state) { - const stateModel = alertDef.getStateDisplayModel(state); - rule.state = state; - rule.stateText = stateModel.text; - rule.stateIcon = stateModel.iconClass; - rule.stateClass = stateModel.stateClass; - rule.stateAge = moment(rule.newStateDate) - .fromNow() - .replace(' ago', ''); -} - -export const getAlertRules = async (): Promise => { - try { - const rules = await getBackendSrv().get('/api/alerts', {}); - - for (const rule of rules) { - setStateFields(rule, rule.state); - - if (rule.state !== 'paused') { - if (rule.executionError) { - rule.info = 'Execution Error: ' + rule.executionError; - } - if (rule.evalData && rule.evalData.noData) { - rule.info = 'Query returned no data'; - } - } - } - - return rules; - } catch (error) { - console.error(error); - throw error; - } -}; diff --git a/public/app/features/alerting/state/reducers.ts b/public/app/features/alerting/state/reducers.ts new file mode 100644 index 00000000000..0718c511106 --- /dev/null +++ b/public/app/features/alerting/state/reducers.ts @@ -0,0 +1,46 @@ +import { Action } from './actions'; +import { AlertRule } from 'app/types'; +import alertDef from './alertDef'; +import moment from 'moment'; + +export const initialState: AlertRule[] = []; + +export function setStateFields(rule, state) { + const stateModel = alertDef.getStateDisplayModel(state); + rule.state = state; + rule.stateText = stateModel.text; + rule.stateIcon = stateModel.iconClass; + rule.stateClass = stateModel.stateClass; + rule.stateAge = moment(rule.newStateDate) + .fromNow() + .replace(' ago', ''); +} + +export const alertRulesReducer = (state = initialState, action: Action): AlertRule[] => { + switch (action.type) { + case 'LOAD_ALERT_RULES': { + const alertRules = action.payload; + + for (const rule of alertRules) { + setStateFields(rule, rule.state); + + if (rule.state !== 'paused') { + if (rule.executionError) { + rule.info = 'Execution Error: ' + rule.executionError; + } + if (rule.evalData && rule.evalData.noData) { + rule.info = 'Query returned no data'; + } + } + } + + return alertRules; + } + } + + return state; +}; + +export default { + alertRules: alertRulesReducer, +}; diff --git a/public/app/stores/configureStore.ts b/public/app/stores/configureStore.ts index 3a7d16da76d..232f2e30cb8 100644 --- a/public/app/stores/configureStore.ts +++ b/public/app/stores/configureStore.ts @@ -2,9 +2,11 @@ import { createStore, applyMiddleware, compose, combineReducers } from 'redux'; import thunk from 'redux-thunk'; import { createLogger } from 'redux-logger'; import sharedReducers from 'app/core/reducers'; +import alertingReducers from 'app/features/alerting/state/reducers'; const rootReducer = combineReducers({ - ...sharedReducers + ...sharedReducers, + ...alertingReducers, }); export let store; diff --git a/public/app/types/index.ts b/public/app/types/index.ts index 930c08c9eb0..a409f586f33 100644 --- a/public/app/types/index.ts +++ b/public/app/types/index.ts @@ -1,3 +1,7 @@ +// +// Location +// + export interface LocationUpdate { path?: string; query?: UrlQueryMap; @@ -14,6 +18,30 @@ export interface LocationState { export type UrlQueryValue = string | number | boolean | string[] | number[] | boolean[]; export type UrlQueryMap = { [s: string]: UrlQueryValue }; +// +// Alerting +// + +export interface AlertRule { + id: number; + dashboardId: number; + panelId: number; + name: string; + state: string; + stateText: string; + stateIcon: string; + stateClass: string; + stateAge: string; + info?: string; + url: string; + executionError?: string; + evalData?: { noData: boolean }; +} + +// +// NavModel +// + export interface NavModelItem { text: string; url: string; @@ -37,7 +65,12 @@ export interface NavModel { export type NavIndex = { [s: string]: NavModelItem }; +// +// Store +// + export interface StoreState { navIndex: NavIndex; location: LocationState; + alertRules: AlertRule[]; } From 3fd707f321a7c2fbb8081f87b6bc62122e9208da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Sun, 2 Sep 2018 12:08:31 -0700 Subject: [PATCH 584/786] redux: progress --- .../app/features/alerting/AlertRuleList.tsx | 28 ++++++++++--------- public/app/features/alerting/state/actions.ts | 6 ++-- 2 files changed, 19 insertions(+), 15 deletions(-) diff --git a/public/app/features/alerting/AlertRuleList.tsx b/public/app/features/alerting/AlertRuleList.tsx index faa46945536..03bafe119b0 100644 --- a/public/app/features/alerting/AlertRuleList.tsx +++ b/public/app/features/alerting/AlertRuleList.tsx @@ -15,12 +15,11 @@ interface Props { alertRules: AlertRule[]; updateLocation: typeof updateLocation; getAlertRulesAsync: typeof getAlertRulesAsync; + stateFilter: string; } interface State { - rules: AlertRule[]; search: string; - stateFilter: string; } export class AlertRuleList extends PureComponent { @@ -37,29 +36,31 @@ export class AlertRuleList extends PureComponent { super(props); this.state = { - rules: [], search: '', - stateFilter: '', }; } componentDidMount() { - this.fetchRules(); + this.fetchRules(this.getStateFilter()); } onStateFilterChanged = evt => { this.props.updateLocation({ query: { state: evt.target.value }, }); - this.fetchRules(); + this.fetchRules(evt.target.value); }; - async fetchRules() { - await this.props.getAlertRulesAsync(); + getStateFilter(): string { + const { stateFilter } = this.props; + if (stateFilter) { + return stateFilter.toString(); + } + return 'all'; + } - // this.props.alertList.loadRules({ - // state: this.props.view.query.get('state') || 'all', - // }); + async fetchRules(stateFilter: string) { + await this.props.getAlertRulesAsync({ state: stateFilter }); } onOpenHowTo = () => { @@ -76,7 +77,7 @@ export class AlertRuleList extends PureComponent { render() { const { navModel, alertRules } = this.props; - const { search, stateFilter } = this.state; + const { search } = this.state; return (
    @@ -99,7 +100,7 @@ export class AlertRuleList extends PureComponent {
    - {this.stateFilters.map(AlertStateFilterOption)}
    @@ -200,6 +201,7 @@ export class AlertRuleItem extends React.Component { const mapStateToProps = (state: StoreState) => ({ navModel: getNavModel(state.navIndex, 'alert-list'), alertRules: state.alertRules, + stateFilter: state.location.query.state, }); const mapDispatchToProps = { diff --git a/public/app/features/alerting/state/actions.ts b/public/app/features/alerting/state/actions.ts index 0f9caa9d47f..9103b34e81d 100644 --- a/public/app/features/alerting/state/actions.ts +++ b/public/app/features/alerting/state/actions.ts @@ -14,9 +14,11 @@ export const loadAlertRules = (rules: AlertRule[]): LoadAlertRulesAction => ({ export type Action = LoadAlertRulesAction; -export const getAlertRulesAsync = () => async (dispatch: Dispatch): Promise => { +export const getAlertRulesAsync = (options: { state: string }) => async ( + dispatch: Dispatch +): Promise => { try { - const rules = await getBackendSrv().get('/api/alerts', {}); + const rules = await getBackendSrv().get('/api/alerts', options); dispatch(loadAlertRules(rules)); return rules; } catch (error) { From 42aaa2b90746eee050bab8495b1d007277e2863f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Sun, 2 Sep 2018 12:14:41 -0700 Subject: [PATCH 585/786] redux: improved state handling --- public/app/features/alerting/AlertRuleList.tsx | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/public/app/features/alerting/AlertRuleList.tsx b/public/app/features/alerting/AlertRuleList.tsx index 03bafe119b0..77e5af520fc 100644 --- a/public/app/features/alerting/AlertRuleList.tsx +++ b/public/app/features/alerting/AlertRuleList.tsx @@ -41,14 +41,20 @@ export class AlertRuleList extends PureComponent { } componentDidMount() { - this.fetchRules(this.getStateFilter()); + console.log('did mount'); + this.fetchRules(); + } + + componentDidUpdate(prevProps: Props) { + if (prevProps.stateFilter !== this.props.stateFilter) { + this.fetchRules(); + } } onStateFilterChanged = evt => { this.props.updateLocation({ query: { state: evt.target.value }, }); - this.fetchRules(evt.target.value); }; getStateFilter(): string { @@ -59,8 +65,8 @@ export class AlertRuleList extends PureComponent { return 'all'; } - async fetchRules(stateFilter: string) { - await this.props.getAlertRulesAsync({ state: stateFilter }); + async fetchRules() { + await this.props.getAlertRulesAsync({ state: this.getStateFilter() }); } onOpenHowTo = () => { From 0d25aa08fa089bc40da98dd37f72737f63b45e95 Mon Sep 17 00:00:00 2001 From: Jordan Hamel Date: Sun, 2 Sep 2018 22:58:45 -0700 Subject: [PATCH 586/786] update wording and punctuation (#13113) * word fix support >> supports says >> ways * : for emphasis --- docs/sources/tutorials/ha_setup.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/sources/tutorials/ha_setup.md b/docs/sources/tutorials/ha_setup.md index 9ae2989f6e6..0f138b20a17 100644 --- a/docs/sources/tutorials/ha_setup.md +++ b/docs/sources/tutorials/ha_setup.md @@ -27,7 +27,7 @@ Grafana will now persist all long term data in the database. How to configure th ## User sessions The second thing to consider is how to deal with user sessions and how to configure your load balancer infront of Grafana. -Grafana support two says of storing session data locally on disk or in a database/cache-server. +Grafana supports two ways of storing session data: locally on disk or in a database/cache-server. If you want to store sessions on disk you can use `sticky sessions` in your load balanacer. If you prefer to store session data in a database/cache-server you can use any stateless routing strategy in your load balancer (ex round robin or least connections). From 7837ee446690e49e7d6eb63edfe164be5e88d2df Mon Sep 17 00:00:00 2001 From: Patrick O'Carroll Date: Mon, 3 Sep 2018 11:00:46 +0200 Subject: [PATCH 587/786] Updated rules for variable name (#13106) * updated rules for variable name and fixed files that didn't follow new rules * fixed test so it uses new rule * made exceptions to rule in interval_variable --- public/app/app.ts | 4 +- public/app/core/config.ts | 4 +- public/app/core/utils/outline.ts | 22 +-- public/app/core/utils/ticks.ts | 10 +- public/app/core/utils/version.ts | 4 +- .../features/annotations/events_processing.ts | 32 ++-- .../app/features/dashboard/dashboard_ctrl.ts | 2 +- .../dashboard/dashboard_loader_srv.ts | 10 +- .../features/dashboard/specs/repeat.test.ts | 36 ++--- public/app/features/org/profile_ctrl.ts | 4 +- .../features/templating/interval_variable.ts | 4 +- .../datasource/elasticsearch/datasource.ts | 12 +- .../datasource/elasticsearch/metric_agg.ts | 4 +- .../elasticsearch/specs/query_builder.test.ts | 8 +- .../plugins/datasource/graphite/datasource.ts | 12 +- .../plugins/datasource/opentsdb/datasource.ts | 50 +++--- .../prometheus/metric_find_query.ts | 28 ++-- .../prometheus/result_transformer.ts | 8 +- public/app/plugins/panel/graph/graph.ts | 12 +- .../app/plugins/panel/graph/graph_tooltip.ts | 6 +- public/app/plugins/panel/graph/histogram.ts | 4 +- .../app/plugins/panel/heatmap/heatmap_ctrl.ts | 4 +- .../panel/heatmap/heatmap_data_converter.ts | 38 ++--- public/app/plugins/panel/heatmap/rendering.ts | 146 +++++++++--------- public/app/plugins/panel/table/renderer.ts | 12 +- tslint.json | 9 +- 26 files changed, 246 insertions(+), 239 deletions(-) diff --git a/public/app/app.ts b/public/app/app.ts index d9e31018af9..77f56264504 100644 --- a/public/app/app.ts +++ b/public/app/app.ts @@ -105,9 +105,9 @@ export class GrafanaApp { 'react', ]; - const module_types = ['controllers', 'directives', 'factories', 'services', 'filters', 'routes']; + const moduleTypes = ['controllers', 'directives', 'factories', 'services', 'filters', 'routes']; - _.each(module_types, type => { + _.each(moduleTypes, type => { const moduleName = 'grafana.' + type; this.useModule(angular.module(moduleName, [])); }); diff --git a/public/app/core/config.ts b/public/app/core/config.ts index f522c6340e6..86720ed5dcc 100644 --- a/public/app/core/config.ts +++ b/public/app/core/config.ts @@ -11,9 +11,9 @@ export class Settings { datasources: any; panels: any; appSubUrl: string; - window_title_prefix: string; + windowTitlePrefix: string; buildInfo: BuildInfo; - new_panel_title: string; + newPanelTitle: string; bootData: any; externalUserMngLinkUrl: string; externalUserMngLinkName: string; diff --git a/public/app/core/utils/outline.ts b/public/app/core/utils/outline.ts index ebd4258c66b..975dc1c8b2d 100644 --- a/public/app/core/utils/outline.ts +++ b/public/app/core/utils/outline.ts @@ -2,32 +2,32 @@ function outlineFixer() { const d: any = document; - const style_element = d.createElement('STYLE'); - const dom_events = 'addEventListener' in d; + const styleElement = d.createElement('STYLE'); + const domEvents = 'addEventListener' in d; - const add_event_listener = function(type, callback) { + const addEventListener = function(type, callback) { // Basic cross-browser event handling - if (dom_events) { + if (domEvents) { d.addEventListener(type, callback); } else { d.attachEvent('on' + type, callback); } }; - const set_css = function(css_text) { + const setCss = function(cssText) { // Handle setting of