From 3e0b92d6feca7da9603ca2038099fbdc6e6c40e6 Mon Sep 17 00:00:00 2001 From: Chris Burkhart Date: Mon, 24 Jul 2017 09:55:57 -0700 Subject: [PATCH 01/83] Enable datasources to be able to round off to a UTC day properly --- public/app/core/utils/datemath.ts | 8 ++++++-- public/app/features/dashboard/time_srv.ts | 5 +++-- public/app/features/panel/metrics_panel_ctrl.ts | 3 +++ public/test/core/utils/datemath_specs.ts | 8 ++++++++ 4 files changed, 20 insertions(+), 4 deletions(-) diff --git a/public/app/core/utils/datemath.ts b/public/app/core/utils/datemath.ts index 2aa793016ba..f5608443f49 100644 --- a/public/app/core/utils/datemath.ts +++ b/public/app/core/utils/datemath.ts @@ -5,7 +5,7 @@ import moment from 'moment'; var units = ['y', 'M', 'w', 'd', 'h', 'm', 's']; -export function parse(text, roundUp?) { +export function parse(text, roundUp?, timezone?) { if (!text) { return undefined; } if (moment.isMoment(text)) { return text; } if (_.isDate(text)) { return moment(text); } @@ -16,7 +16,11 @@ export function parse(text, roundUp?) { var parseString; if (text.substring(0, 3) === 'now') { - time = moment(); + if (timezone === 'utc') { + time = moment.utc(); + } else { + time = moment(); + } mathString = text.substring('now'.length); } else { index = text.indexOf('||'); diff --git a/public/app/features/dashboard/time_srv.ts b/public/app/features/dashboard/time_srv.ts index abde4152b63..ef5dead40a7 100644 --- a/public/app/features/dashboard/time_srv.ts +++ b/public/app/features/dashboard/time_srv.ts @@ -199,10 +199,11 @@ class TimeSrv { from: moment.isMoment(this.time.from) ? moment(this.time.from) : this.time.from, to: moment.isMoment(this.time.to) ? moment(this.time.to) : this.time.to, }; + var timezone = this.dashboard && this.dashboard.getTimezone ? this.dashboard.getTimezone() : 'local'; return { - from: dateMath.parse(raw.from, false), - to: dateMath.parse(raw.to, true), + from: dateMath.parse(raw.from, false, timezone), + to: dateMath.parse(raw.to, true, timezone), raw: raw }; } diff --git a/public/app/features/panel/metrics_panel_ctrl.ts b/public/app/features/panel/metrics_panel_ctrl.ts index 0e94df437a8..1ae24066bb9 100644 --- a/public/app/features/panel/metrics_panel_ctrl.ts +++ b/public/app/features/panel/metrics_panel_ctrl.ts @@ -221,7 +221,10 @@ class MetricsPanelCtrl extends PanelCtrl { "__interval_ms": {text: this.intervalMs, value: this.intervalMs}, }); + var timezone = this.dashboard.getTimezone ? this.dashboard.getTimezone() : 'local'; + var metricsQuery = { + timezone: timezone, panelId: this.panel.id, range: this.range, rangeRaw: this.range.raw, diff --git a/public/test/core/utils/datemath_specs.ts b/public/test/core/utils/datemath_specs.ts index c6096485f65..b919653c2c8 100644 --- a/public/test/core/utils/datemath_specs.ts +++ b/public/test/core/utils/datemath_specs.ts @@ -46,6 +46,14 @@ describe("DateMath", () => { expect(startOfDay).to.be(expected.getTime()); }); + it("now/d on a utc dashboard should be start of the current day in UTC time", () => { + var today = new Date(); + var expected = new Date(Date.UTC(today.getFullYear(), today.getMonth(), today.getDate(), 0, 0, 0, 0)); + + var startOfDay = dateMath.parse('now/d', false, 'utc').valueOf(); + expect(startOfDay).to.be(expected.getTime()); + }); + describe('subtraction', () => { var now; var anchored; From c6f7d34c5528d1638ba5628b7c94373b4386c283 Mon Sep 17 00:00:00 2001 From: Joseph Weigl Date: Thu, 24 Aug 2017 13:40:19 +0200 Subject: [PATCH 02/83] Reorder editorconfig --- .editorconfig | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/.editorconfig b/.editorconfig index 386c27fceb8..146224e7330 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,13 +1,6 @@ # http://editorconfig.org root = true -[*.go] -indent_style = tab -indent_size = 4 -charset = utf-8 -trim_trailing_whitespace = true -insert_final_newline = true - [*] indent_style = space indent_size = 2 @@ -15,5 +8,12 @@ charset = utf-8 trim_trailing_whitespace = true insert_final_newline = true +[*.go] +indent_style = tab +indent_size = 4 +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true + [*.md] trim_trailing_whitespace = false From 9666f45e9c2447db0614f902eb08b3f82673c904 Mon Sep 17 00:00:00 2001 From: Joseph Weigl Date: Thu, 24 Aug 2017 13:40:33 +0200 Subject: [PATCH 03/83] Add values to the hipchat card --- pkg/services/alerting/notifiers/hipchat.go | 31 +++++++++++----------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/pkg/services/alerting/notifiers/hipchat.go b/pkg/services/alerting/notifiers/hipchat.go index 0eb21865b9f..03176c64729 100644 --- a/pkg/services/alerting/notifiers/hipchat.go +++ b/pkg/services/alerting/notifiers/hipchat.go @@ -84,15 +84,13 @@ func (this *HipChatNotifier) Notify(evalContext *alerting.EvalContext) error { return err } - message := evalContext.GetNotificationTitle() + " in state " + evalContext.GetStateModel().Text + "
Check Dasboard" - fields := make([]map[string]interface{}, 0) - message += "
" + attributes := make([]map[string]interface{}, 0) for index, evt := range evalContext.EvalMatches { - message += evt.Metric + " :: " + strconv.FormatFloat(evt.Value.Float64, 'f', -1, 64) + "
" - fields = append(fields, map[string]interface{}{ - "title": evt.Metric, - "value": evt.Value, - "short": true, + attributes = append(attributes, map[string]interface{}{ + "label": evt.Metric, + "value": map[string]interface{}{ + "label": strconv.FormatFloat(evt.Value.Float64, 'f', -1, 64), + }, }) if index > maxFieldCount { break @@ -100,13 +98,15 @@ func (this *HipChatNotifier) Notify(evalContext *alerting.EvalContext) error { } if evalContext.Error != nil { - fields = append(fields, map[string]interface{}{ - "title": "Error message", - "value": evalContext.Error.Error(), - "short": false, + attributes = append(attributes, map[string]interface{}{ + "label": "Error message", + "value": map[string]interface{}{ + "label": evalContext.Error.Error(), + }, }) } + message := "" if evalContext.Rule.State != models.AlertStateOK { //dont add message when going back to alert state ok. message += " " + evalContext.Rule.Message } @@ -123,15 +123,16 @@ func (this *HipChatNotifier) Notify(evalContext *alerting.EvalContext) error { // Add a card with link to the dashboard card := map[string]interface{}{ - "style": "link", + "style": "application", "url": ruleUrl, "id": "1", "title": evalContext.GetNotificationTitle(), - "description": evalContext.GetNotificationTitle() + " in state " + evalContext.GetStateModel().Text, + "description": message, "icon": map[string]interface{}{ "url": "https://grafana.com/assets/img/fav32.png", }, - "date": evalContext.EndTime.Unix(), + "date": evalContext.EndTime.Unix(), + "attributes": attributes, } body := map[string]interface{}{ From 81d3ab37c372642ebf151d05378663d9402974f3 Mon Sep 17 00:00:00 2001 From: Joseph Weigl Date: Thu, 24 Aug 2017 14:52:23 +0200 Subject: [PATCH 04/83] Add thumbnail to card --- pkg/services/alerting/notifiers/hipchat.go | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/pkg/services/alerting/notifiers/hipchat.go b/pkg/services/alerting/notifiers/hipchat.go index 03176c64729..757120db25b 100644 --- a/pkg/services/alerting/notifiers/hipchat.go +++ b/pkg/services/alerting/notifiers/hipchat.go @@ -134,6 +134,14 @@ func (this *HipChatNotifier) Notify(evalContext *alerting.EvalContext) error { "date": evalContext.EndTime.Unix(), "attributes": attributes, } + if len(evalContext.ImagePublicUrl) > 0 { + card["thumbnail"] = map[string]interface{}{ + "url": evalContext.ImagePublicUrl, + "url@2x": evalContext.ImagePublicUrl, + "width": 1193, + "height": 564, + } + } body := map[string]interface{}{ "message": message, From 84c6caabc5b06a8fdb342714e3fed2cd5adc6774 Mon Sep 17 00:00:00 2001 From: Alin Sinpalean Date: Mon, 28 Aug 2017 15:45:51 +0200 Subject: [PATCH 05/83] Prometheus: Fix actual step computation logic when a min_step is specified and the range is longer than min_step * 11000. --- public/app/plugins/datasource/prometheus/datasource.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/app/plugins/datasource/prometheus/datasource.ts b/public/app/plugins/datasource/prometheus/datasource.ts index 36fc1e5de4b..9e416de44c7 100644 --- a/public/app/plugins/datasource/prometheus/datasource.ts +++ b/public/app/plugins/datasource/prometheus/datasource.ts @@ -128,7 +128,7 @@ export function PrometheusDatasource(instanceSettings, $q, backendSrv, templateS // Prometheus drop query if range/step > 11000 // calibrate step if it is too big if (step !== 0 && range / step > 11000) { - return Math.ceil(range / 11000); + step = Math.ceil(range / 11000); } return Math.max(step, autoStep); }; From 13eb0c1ece082bb2db7c791ba71eb5f450d7118a Mon Sep 17 00:00:00 2001 From: Alin Sinpalean Date: Thu, 7 Sep 2017 22:14:37 +0200 Subject: [PATCH 06/83] Fix kbn.round_interval for exact intervals. --- public/app/core/utils/kbn.js | 58 ++++++++++++++--------------- public/test/core/utils/kbn_specs.js | 14 +++++++ 2 files changed, 43 insertions(+), 29 deletions(-) diff --git a/public/app/core/utils/kbn.js b/public/app/core/utils/kbn.js index 37888fb10ac..670051e57d1 100644 --- a/public/app/core/utils/kbn.js +++ b/public/app/core/utils/kbn.js @@ -17,88 +17,88 @@ function($, _) { kbn.round_interval = function(interval) { switch (true) { // 0.015s - case (interval <= 15): + case (interval < 15): return 10; // 0.01s // 0.035s - case (interval <= 35): + case (interval < 35): return 20; // 0.02s // 0.075s - case (interval <= 75): + case (interval < 75): return 50; // 0.05s // 0.15s - case (interval <= 150): + case (interval < 150): return 100; // 0.1s // 0.35s - case (interval <= 350): + case (interval < 350): return 200; // 0.2s // 0.75s - case (interval <= 750): + case (interval < 750): return 500; // 0.5s // 1.5s - case (interval <= 1500): + case (interval < 1500): return 1000; // 1s // 3.5s - case (interval <= 3500): + case (interval < 3500): return 2000; // 2s // 7.5s - case (interval <= 7500): + case (interval < 7500): return 5000; // 5s // 12.5s - case (interval <= 12500): + case (interval < 12500): return 10000; // 10s // 17.5s - case (interval <= 17500): + case (interval < 17500): return 15000; // 15s // 25s - case (interval <= 25000): + case (interval < 25000): return 20000; // 20s // 45s - case (interval <= 45000): + case (interval < 45000): return 30000; // 30s // 1.5m - case (interval <= 90000): + case (interval < 90000): return 60000; // 1m // 3.5m - case (interval <= 210000): + case (interval < 210000): return 120000; // 2m // 7.5m - case (interval <= 450000): + case (interval < 450000): return 300000; // 5m // 12.5m - case (interval <= 750000): + case (interval < 750000): return 600000; // 10m // 12.5m - case (interval <= 1050000): + case (interval < 1050000): return 900000; // 15m // 25m - case (interval <= 1500000): + case (interval < 1500000): return 1200000; // 20m // 45m - case (interval <= 2700000): + case (interval < 2700000): return 1800000; // 30m // 1.5h - case (interval <= 5400000): + case (interval < 5400000): return 3600000; // 1h // 2.5h - case (interval <= 9000000): + case (interval < 9000000): return 7200000; // 2h // 4.5h - case (interval <= 16200000): + case (interval < 16200000): return 10800000; // 3h // 9h - case (interval <= 32400000): + case (interval < 32400000): return 21600000; // 6h // 24h - case (interval <= 86400000): + case (interval < 86400000): return 43200000; // 12h // 48h - case (interval <= 172800000): + case (interval < 172800000): return 86400000; // 24h // 1w - case (interval <= 604800000): + case (interval < 604800000): return 86400000; // 24h // 3w - case (interval <= 1814400000): + case (interval < 1814400000): return 604800000; // 1w // 2y case (interval < 3628800000): @@ -134,7 +134,7 @@ function($, _) { return nummilliseconds + 'ms'; } - return 'less then a millisecond'; //'just now' //or other string you like; + return 'less than a millisecond'; //'just now' //or other string you like; }; kbn.to_percent = function(number,outof) { diff --git a/public/test/core/utils/kbn_specs.js b/public/test/core/utils/kbn_specs.js index 0b7f4b4f2bc..31fb25cb578 100644 --- a/public/test/core/utils/kbn_specs.js +++ b/public/test/core/utils/kbn_specs.js @@ -167,6 +167,20 @@ define([ var res = kbn.calculateInterval(range, 900, '>15ms'); expect(res.interval).to.be('15ms'); }); + + it('1d 1 resolution', function() { + var range = { from: dateMath.parse('now-1d'), to: dateMath.parse('now') }; + var res = kbn.calculateInterval(range, 1, null); + expect(res.interval).to.be('1d'); + expect(res.intervalMs).to.be(86400000); + }); + + it('86399s 1 resolution', function() { + var range = { from: dateMath.parse('now-86399s'), to: dateMath.parse('now') }; + var res = kbn.calculateInterval(range, 1, null); + expect(res.interval).to.be('12h'); + expect(res.intervalMs).to.be(43200000); + }); }); describe('hex', function() { From 2a62374a61e8e10d1141914038325eb8e856e46b Mon Sep 17 00:00:00 2001 From: Alin Sinpalean Date: Tue, 12 Sep 2017 16:36:27 +0200 Subject: [PATCH 07/83] Prometheus: Rework the interaction between auto interval (computed based on graph resolution), min interval (where specified, per query) and intervalFactor (AKA resolution, where specified, per query). As a bonus, have and reflect the actual interval (not the auto interval), taking into account min interval and Prometheus' 11k data points limit. --- .../datasource/prometheus/datasource.ts | 40 ++- .../prometheus/specs/datasource_specs.ts | 262 ++++++++++++++++++ 2 files changed, 290 insertions(+), 12 deletions(-) diff --git a/public/app/plugins/datasource/prometheus/datasource.ts b/public/app/plugins/datasource/prometheus/datasource.ts index dd071d09fe6..7e0758a253c 100644 --- a/public/app/plugins/datasource/prometheus/datasource.ts +++ b/public/app/plugins/datasource/prometheus/datasource.ts @@ -98,14 +98,29 @@ export class PrometheusDatasource { activeTargets.push(target); var query: any = {}; - query.expr = this.templateSrv.replace(target.expr, options.scopedVars, self.interpolateQueryExpr); - query.requestId = options.panelId + target.refId; - var interval = this.templateSrv.replace(target.interval, options.scopedVars) || options.interval; + var interval = this.intervalSeconds(options.interval); + // Minimum interval ("Min step"), if specified for the query. or same as interval otherwise + var minInterval = this.intervalSeconds(this.templateSrv.replace(target.interval, options.scopedVars) || options.interval); var intervalFactor = target.intervalFactor || 1; - target.step = query.step = this.calculateInterval(interval, intervalFactor); var range = Math.ceil(end - start); - target.step = query.step = this.adjustStep(query.step, this.intervalSeconds(options.interval), range); + // Adjust the interval to take into account any specified minimum plus Prometheus limitations + var adjustedInterval = this.adjustInterval(interval, minInterval, range, intervalFactor); + + var scopedVars = options.scopedVars; + // If the interval was adjusted, make a shallow copy of scopedVars with updated interval vars + if (interval !== adjustedInterval) { + interval = adjustedInterval; + scopedVars = Object.assign({}, options.scopedVars, { + "__interval": {text: interval + "s", value: interval + "s"}, + "__interval_ms": {text: interval * 1000, value: interval * 1000}, + }); + } + target.step = query.step = interval * intervalFactor; + + // Only replace vars in expression after having (possibly) updated interval vars + query.expr = this.templateSrv.replace(target.expr, scopedVars, self.interpolateQueryExpr); + query.requestId = options.panelId + target.refId; queries.push(query); } @@ -140,13 +155,14 @@ export class PrometheusDatasource { }); } - adjustStep(step, autoStep, range) { - // Prometheus drop query if range/step > 11000 - // calibrate step if it is too big - if (step !== 0 && range / step > 11000) { - step = Math.ceil(range / 11000); + adjustInterval(interval, minInterval, range, intervalFactor) { + interval = Math.max(interval, minInterval); + // Prometheus will drop queries that might return more than 11000 data points. + // Calibrate interval if it is too small. + if (interval !== 0 && range / intervalFactor / interval > 11000) { + interval = Math.ceil(range / intervalFactor / 11000); } - return Math.max(step, autoStep); + return interval; } performTimeSeriesQuery(query, start, end) { @@ -206,7 +222,7 @@ export class PrometheusDatasource { var end = this.getPrometheusTime(options.range.to, true); var query = { expr: interpolated, - step: this.adjustStep(kbn.interval_to_seconds(step), 0, Math.ceil(end - start)) + 's' + step: this.adjustInterval(kbn.interval_to_seconds(step), 0, Math.ceil(end - start), 1) + 's' }; var self = this; diff --git a/public/app/plugins/datasource/prometheus/specs/datasource_specs.ts b/public/app/plugins/datasource/prometheus/specs/datasource_specs.ts index 9588090c7bb..1e6fff2607f 100644 --- a/public/app/plugins/datasource/prometheus/specs/datasource_specs.ts +++ b/public/app/plugins/datasource/prometheus/specs/datasource_specs.ts @@ -195,4 +195,266 @@ describe('PrometheusDatasource', function() { ); }); }); + describe('The "step" query parameter', function() { + var response = { + status: "success", + data: { + resultType: "matrix", + result: [] + } + }; + + beforeEach(function() { + ctx.$httpBackend.flush(); + }); + + it('should be min interval when greater than auto interval', function() { + var query = { + range: { from: moment(1443438674760), to: moment(1443460274760) }, + targets: [{ + expr: 'test', + interval: '10s' + }], + interval: '5s' + }; + var urlExpected = 'proxied/api/v1/query_range?query=test' + + '&start=1443438675&end=1443460275&step=10'; + 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 = { + range: { from: moment(1443438674760), to: moment(1443460274760) }, + targets: [{ + expr: 'test', + interval: '5s' + }], + interval: '10s' + }; + var urlExpected = 'proxied/api/v1/query_range?query=test' + + '&start=1443438675&end=1443460275&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 = { + range: { from: moment(1443438674760), to: moment(1443460274760) }, + targets: [{ expr: 'test' }], + interval: '1s' + }; + var urlExpected = 'proxied/api/v1/query_range?query=test' + + '&start=1443438675&end=1443460275&step=2'; + ctx.$httpBackend.expect('GET', urlExpected).respond(response); + ctx.ds.query(query); + ctx.$httpBackend.verifyNoOutstandingExpectation(); + }); + it('should apply intervalFactor to min interval when greater', function() { + var query = { + range: { from: moment(1443438674760), to: moment(1443460274760) }, + targets: [{ + expr: 'test', + interval: '10s', + intervalFactor: 10 + }], + interval: '5s' + }; + var urlExpected = 'proxied/api/v1/query_range?query=test' + + '&start=1443438675&end=1443460275&step=100'; + 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 = { + range: { from: moment(1443438674760), to: moment(1443460274760) }, + targets: [{ + expr: 'test', + interval: '5s', + intervalFactor: 10 + }], + interval: '10s' + }; + var urlExpected = 'proxied/api/v1/query_range?query=test' + + '&start=1443438675&end=1443460275&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: moment(1443438674760), to: moment(1444043474760) }, + targets: [{ + expr: 'test', + intervalFactor: 10 + }], + interval: '10s' + }; + var urlExpected = 'proxied/api/v1/query_range?query=test' + + '&start=1443438675&end=1444043475&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: moment(1443438674760), to: moment(1444043474760) }, + targets: [{ + expr: 'test', + intervalFactor: 10 + }], + interval: '5s' + }; + var urlExpected = 'proxied/api/v1/query_range?query=test' + + '&start=1443438675&end=1444043475&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: [] + } + }; + + beforeEach(function() { + ctx.$httpBackend.flush(); + }); + + it('should be unchanged when auto interval is greater than min interval', function() { + var query = { + range: { from: moment(1443438674760), to: moment(1443460274760) }, + 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=1443438675&end=1443460275&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 = { + range: { from: moment(1443438674760), to: moment(1443460274760) }, + 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=1443438675&end=1443460275&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 ignore intervalFactor', function() { + var query = { + range: { from: moment(1443438674760), to: moment(1443460274760) }, + 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[10s])') + + '&start=1443438675&end=1443460275&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 ignore intervalFactor', function() { + var query = { + range: { from: moment(1443438674760), to: moment(1443460274760) }, + 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[10s])') + + '&start=1443438675&end=1443460275&step=100'; + 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: moment(1443438674760), to: moment(1444043474760) }, + targets: [{ + expr: 'rate(test[$__interval])', + 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[6s])') + + '&start=1443438675&end=1444043475&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); + }); + }); }); From 40b74e666446dc93c222899c8d62c68787f7dda2 Mon Sep 17 00:00:00 2001 From: Mitsuhiro Tanda Date: Wed, 6 Sep 2017 20:50:32 +0900 Subject: [PATCH 08/83] (prometheus) support label name completion --- .../datasource/prometheus/completer.ts | 105 ++++++++++++++++-- .../datasource/prometheus/datasource.ts | 5 + 2 files changed, 102 insertions(+), 8 deletions(-) diff --git a/public/app/plugins/datasource/prometheus/completer.ts b/public/app/plugins/datasource/prometheus/completer.ts index e1b0984399d..eaf3f034d67 100644 --- a/public/app/plugins/datasource/prometheus/completer.ts +++ b/public/app/plugins/datasource/prometheus/completer.ts @@ -1,11 +1,15 @@ /// import {PrometheusDatasource} from "./datasource"; +import _ from 'lodash'; export class PromCompleter { + labelNameCache: any; + identifierRegexps = [/[\[\]a-zA-Z_0-9=]/]; constructor(private datasource: PrometheusDatasource) { + this.labelNameCache = {}; } getCompletions(editor, session, pos, prefix, callback) { @@ -13,14 +17,31 @@ export class PromCompleter { switch (token.type) { case 'label.name': - callback(null, ['instance', 'job'].map(function (key) { - return { - caption: key, - value: key, - meta: "label name", - score: Number.MAX_VALUE - }; - })); + var metricName = this.findMetricName(session, pos.row, pos.column); + if (!metricName) { + callback(null, this.transformToCompletions(['__name__', 'instance', 'job'])); + return; + } + + if (this.labelNameCache[metricName]) { + callback(null, this.labelNameCache[metricName]); + return; + } + + var op = '=~'; + if (/[a-zA-Z_:][a-zA-Z0-9_:]*/.test(metricName)) { + op = '='; + } + var expr = '{__name__' + op + '"' + metricName + '"}'; + this.datasource.performInstantQuery({ expr: expr }, new Date().getTime() / 1000).then(response => { + var labelNames = this.transformToCompletions( + _.uniq(_.flatten(response.data.data.result.map(r => { + return Object.keys(r.metric); + }))) + ); + this.labelNameCache[metricName] = labelNames; + callback(null, labelNames); + }); return; case 'label.value': callback(null, []); @@ -57,4 +78,72 @@ export class PromCompleter { }); } + transformToCompletions(words) { + return words.map(name => { + return { + caption: name, + value: name, + meta: "label name", + score: Number.MAX_VALUE + }; + }); + } + + findMetricName(session, row, column) { + var metricName = ''; + + var tokens; + var nameLabelNameToken = this.findToken(session, row, column, 'label.name', '__name__', 'paren.lparen'); + if (nameLabelNameToken) { + tokens = session.getTokens(nameLabelNameToken.row); + var nameLabelValueToken = tokens[nameLabelNameToken.index + 2]; + if (nameLabelValueToken && nameLabelValueToken.type === 'label.value') { + metricName = nameLabelValueToken.value.slice(1, -1); // cut begin/end quotation + } + } else { + var metricNameToken = this.findToken(session, row, column, 'identifier', null, null); + if (metricNameToken) { + tokens = session.getTokens(metricNameToken.row); + if (tokens[metricNameToken.index + 1].type === 'paren.lparen') { + metricName = metricNameToken.value; + } + } + } + + return metricName; + } + + findToken(session, row, column, target, value, guard) { + var tokens, idx; + for (var r = row; r >= 0; r--) { + tokens = session.getTokens(r); + if (r === row) { // current row + var c = 0; + for (idx = 0; idx < tokens.length; idx++) { + c += tokens[idx].value.length; + if (c >= column) { + break; + } + } + } else { + idx = tokens.length - 1; + } + + for (; idx >= 0; idx--) { + if (tokens[idx].type === guard) { + return null; + } + + if (tokens[idx].type === target + && (!value || tokens[idx].value === value)) { + tokens[idx].row = r; + tokens[idx].index = idx; + return tokens[idx]; + } + } + } + + return null; + } + } diff --git a/public/app/plugins/datasource/prometheus/datasource.ts b/public/app/plugins/datasource/prometheus/datasource.ts index dd071d09fe6..ad03607baf0 100644 --- a/public/app/plugins/datasource/prometheus/datasource.ts +++ b/public/app/plugins/datasource/prometheus/datasource.ts @@ -158,6 +158,11 @@ export class PrometheusDatasource { return this._request('GET', url, query.requestId); } + performInstantQuery(query, time) { + var url = '/api/v1/query?query=' + encodeURIComponent(query.expr) + '&time=' + time; + return this._request('GET', url, query.requestId); + } + performSuggestQuery(query, cache = false) { var url = '/api/v1/label/__name__/values'; From d530ccff1c2e40cc51f19d8734da0ec64e07ee6e Mon Sep 17 00:00:00 2001 From: Mitsuhiro Tanda Date: Mon, 11 Sep 2017 22:09:05 +0900 Subject: [PATCH 09/83] (prometheus) support label value completion --- .../datasource/prometheus/completer.ts | 68 +++++++++++++++---- 1 file changed, 55 insertions(+), 13 deletions(-) diff --git a/public/app/plugins/datasource/prometheus/completer.ts b/public/app/plugins/datasource/prometheus/completer.ts index eaf3f034d67..c6af64584d4 100644 --- a/public/app/plugins/datasource/prometheus/completer.ts +++ b/public/app/plugins/datasource/prometheus/completer.ts @@ -4,22 +4,27 @@ import {PrometheusDatasource} from "./datasource"; import _ from 'lodash'; export class PromCompleter { + labelQueryCache: any; labelNameCache: any; + labelValueCache: any; identifierRegexps = [/[\[\]a-zA-Z_0-9=]/]; constructor(private datasource: PrometheusDatasource) { + this.labelQueryCache = {}; this.labelNameCache = {}; + this.labelValueCache = {}; } getCompletions(editor, session, pos, prefix, callback) { let token = session.getTokenAt(pos.row, pos.column); + var metricName; switch (token.type) { case 'label.name': - var metricName = this.findMetricName(session, pos.row, pos.column); + metricName = this.findMetricName(session, pos.row, pos.column); if (!metricName) { - callback(null, this.transformToCompletions(['__name__', 'instance', 'job'])); + callback(null, this.transformToCompletions(['__name__', 'instance', 'job'], 'label name')); return; } @@ -28,23 +33,45 @@ export class PromCompleter { return; } - var op = '=~'; - if (/[a-zA-Z_:][a-zA-Z0-9_:]*/.test(metricName)) { - op = '='; - } - var expr = '{__name__' + op + '"' + metricName + '"}'; - this.datasource.performInstantQuery({ expr: expr }, new Date().getTime() / 1000).then(response => { + this.getLabelNameAndValueForMetric(metricName).then(result => { var labelNames = this.transformToCompletions( - _.uniq(_.flatten(response.data.data.result.map(r => { + _.uniq(_.flatten(result.map(r => { return Object.keys(r.metric); }))) - ); + , 'label name'); this.labelNameCache[metricName] = labelNames; callback(null, labelNames); }); return; case 'label.value': - callback(null, []); + metricName = this.findMetricName(session, pos.row, pos.column); + if (!metricName) { + callback(null, []); + return; + } + + var labelNameToken = this.findToken(session, pos.row, pos.column, 'label.name', null, 'paren.lparen'); + if (!labelNameToken) { + callback(null, []); + return; + } + var labelName = labelNameToken.value; + + if (this.labelValueCache[metricName] && this.labelValueCache[metricName][labelName]) { + callback(null, this.labelValueCache[metricName][labelName]); + return; + } + + this.getLabelNameAndValueForMetric(metricName).then(result => { + var labelValues = this.transformToCompletions( + _.uniq(result.map(r => { + return r.metric[labelName]; + })) + , 'label value'); + this.labelValueCache[metricName] = this.labelValueCache[metricName] || {}; + this.labelValueCache[metricName][labelName] = labelValues; + callback(null, labelValues); + }); return; } @@ -78,12 +105,27 @@ export class PromCompleter { }); } - transformToCompletions(words) { + getLabelNameAndValueForMetric(metricName) { + if (this.labelQueryCache[metricName]) { + return Promise.resolve(this.labelQueryCache[metricName]); + } + var op = '=~'; + if (/[a-zA-Z_:][a-zA-Z0-9_:]*/.test(metricName)) { + op = '='; + } + var expr = '{__name__' + op + '"' + metricName + '"}'; + return this.datasource.performInstantQuery({ expr: expr }, new Date().getTime() / 1000).then(response => { + this.labelQueryCache[metricName] = response.data.data.result; + return response.data.data.result; + }); + } + + transformToCompletions(words, meta) { return words.map(name => { return { caption: name, value: name, - meta: "label name", + meta: meta, score: Number.MAX_VALUE }; }); From 56c0d91ee5245f5b7690f2491aa7045a02dcc85d Mon Sep 17 00:00:00 2001 From: Mitsuhiro Tanda Date: Mon, 11 Sep 2017 23:49:56 +0900 Subject: [PATCH 10/83] follow token name change --- public/app/plugins/datasource/prometheus/completer.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/public/app/plugins/datasource/prometheus/completer.ts b/public/app/plugins/datasource/prometheus/completer.ts index c6af64584d4..577b872c21b 100644 --- a/public/app/plugins/datasource/prometheus/completer.ts +++ b/public/app/plugins/datasource/prometheus/completer.ts @@ -21,7 +21,7 @@ export class PromCompleter { var metricName; switch (token.type) { - case 'label.name': + case 'keyword': metricName = this.findMetricName(session, pos.row, pos.column); if (!metricName) { callback(null, this.transformToCompletions(['__name__', 'instance', 'job'], 'label name')); @@ -43,14 +43,14 @@ export class PromCompleter { callback(null, labelNames); }); return; - case 'label.value': + case 'string': metricName = this.findMetricName(session, pos.row, pos.column); if (!metricName) { callback(null, []); return; } - var labelNameToken = this.findToken(session, pos.row, pos.column, 'label.name', null, 'paren.lparen'); + var labelNameToken = this.findToken(session, pos.row, pos.column, 'keyword', null, 'paren.lparen'); if (!labelNameToken) { callback(null, []); return; @@ -135,11 +135,11 @@ export class PromCompleter { var metricName = ''; var tokens; - var nameLabelNameToken = this.findToken(session, row, column, 'label.name', '__name__', 'paren.lparen'); + var nameLabelNameToken = this.findToken(session, row, column, 'keyword', '__name__', 'paren.lparen'); if (nameLabelNameToken) { tokens = session.getTokens(nameLabelNameToken.row); var nameLabelValueToken = tokens[nameLabelNameToken.index + 2]; - if (nameLabelValueToken && nameLabelValueToken.type === 'label.value') { + if (nameLabelValueToken && nameLabelValueToken.type === 'string') { metricName = nameLabelValueToken.value.slice(1, -1); // cut begin/end quotation } } else { From 1a5e786467afdce45bedbd46b846311889a2384d Mon Sep 17 00:00:00 2001 From: Mitsuhiro Tanda Date: Tue, 12 Sep 2017 09:31:05 +0900 Subject: [PATCH 11/83] fix --- public/app/plugins/datasource/prometheus/completer.ts | 10 +++++----- .../plugins/datasource/prometheus/mode-prometheus.js | 6 +++--- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/public/app/plugins/datasource/prometheus/completer.ts b/public/app/plugins/datasource/prometheus/completer.ts index 577b872c21b..799822eb571 100644 --- a/public/app/plugins/datasource/prometheus/completer.ts +++ b/public/app/plugins/datasource/prometheus/completer.ts @@ -21,7 +21,7 @@ export class PromCompleter { var metricName; switch (token.type) { - case 'keyword': + case 'entity.name.tag': metricName = this.findMetricName(session, pos.row, pos.column); if (!metricName) { callback(null, this.transformToCompletions(['__name__', 'instance', 'job'], 'label name')); @@ -43,14 +43,14 @@ export class PromCompleter { callback(null, labelNames); }); return; - case 'string': + case 'string.quoted': metricName = this.findMetricName(session, pos.row, pos.column); if (!metricName) { callback(null, []); return; } - var labelNameToken = this.findToken(session, pos.row, pos.column, 'keyword', null, 'paren.lparen'); + var labelNameToken = this.findToken(session, pos.row, pos.column, 'entity.name.tag', null, 'paren.lparen'); if (!labelNameToken) { callback(null, []); return; @@ -135,11 +135,11 @@ export class PromCompleter { var metricName = ''; var tokens; - var nameLabelNameToken = this.findToken(session, row, column, 'keyword', '__name__', 'paren.lparen'); + var nameLabelNameToken = this.findToken(session, row, column, 'entity.name.tag', '__name__', 'paren.lparen'); if (nameLabelNameToken) { tokens = session.getTokens(nameLabelNameToken.row); var nameLabelValueToken = tokens[nameLabelNameToken.index + 2]; - if (nameLabelValueToken && nameLabelValueToken.type === 'string') { + if (nameLabelValueToken && nameLabelValueToken.type === 'string.quoted') { metricName = nameLabelValueToken.value.slice(1, -1); // cut begin/end quotation } } else { diff --git a/public/app/plugins/datasource/prometheus/mode-prometheus.js b/public/app/plugins/datasource/prometheus/mode-prometheus.js index 78edbbd30d1..165c1a364c4 100644 --- a/public/app/plugins/datasource/prometheus/mode-prometheus.js +++ b/public/app/plugins/datasource/prometheus/mode-prometheus.js @@ -65,13 +65,13 @@ var PrometheusHighlightRules = function() { regex : "\\s+" } ], "start-label-matcher" : [ { - token : "keyword", + token : "entity.name.tag", regex : '[a-zA-Z_][a-zA-Z0-9_]*' }, { token : "keyword.operator", regex : '=~|=|!~|!=' }, { - token : "string", + token : "string.quoted", regex : '"[^"]*"|\'[^\']*\'' }, { token : "punctuation.operator", @@ -401,7 +401,7 @@ var PrometheusCompletions = function() {}; (function() { this.getCompletions = function(state, session, pos, prefix, callback) { var token = session.getTokenAt(pos.row, pos.column); - if (token.type === 'label.name' || token.type === 'label.value') { + if (token.type === 'entity.name.tag' || token.type === 'string.quoted') { return callback(null, []); } From 6bf81447939246c81e9fa0ae6d485d7f871258a5 Mon Sep 17 00:00:00 2001 From: Mitsuhiro Tanda Date: Tue, 12 Sep 2017 20:05:07 +0900 Subject: [PATCH 12/83] add test for completer --- .../prometheus/specs/completer_specs.ts | 99 +++++++++++++++++-- 1 file changed, 93 insertions(+), 6 deletions(-) diff --git a/public/app/plugins/datasource/prometheus/specs/completer_specs.ts b/public/app/plugins/datasource/prometheus/specs/completer_specs.ts index 903ce0b5d3b..3d0371da7ed 100644 --- a/public/app/plugins/datasource/prometheus/specs/completer_specs.ts +++ b/public/app/plugins/datasource/prometheus/specs/completer_specs.ts @@ -5,23 +5,110 @@ import {PrometheusDatasource} from '../datasource'; describe('Prometheus editor completer', function() { - let editor = {}; - let session = { - getTokenAt: sinon.stub().returns({}), - getLine: sinon.stub().returns(""), + let sessionData = { + currentToken: {}, + tokens: [], + line: '' }; + let session = { + getTokenAt: sinon.stub().returns(sessionData.currentToken), + getTokens: sinon.stub().returns(sessionData.tokens), + getLine: sinon.stub().returns(sessionData.line), + }; + let editor = { session: session }; - let datasourceStub = {}; + let datasourceStub = { + performInstantQuery: sinon.stub().returns(Promise.resolve( + [ + { + metric: { + job: 'node', + instance: 'localhost:9100' + } + } + ] + )), + performSuggestQuery: sinon.stub().returns(Promise.resolve( + [ + 'node_cpu' + ] + )) + }; let completer = new PromCompleter(datasourceStub); describe("When inside brackets", () => { it("Should return range vectors", () => { - completer.getCompletions(editor, session, 10, "[", (s, res) => { + completer.getCompletions(editor, session, { row: 0, column: 10 }, '[', (s, res) => { expect(res[0]).to.eql({caption: '1s', value: '[1s', meta: 'range vector'}); }); }); }); + describe("When inside label matcher, and located at label name", () => { + sessionData = { + currentToken: { type: 'entity.name.tag', value: 'j', index: 2, start: 9 }, + tokens: [ + { type: 'identifier', value: 'node_cpu' }, + { type: 'paren.lparen', value: '{' }, + { type: 'entity.name.tag', value: 'j', index: 2, start: 9 }, + { type: 'paren.rparen', value: '}' } + ], + line: 'node_cpu{j}' + }; + + it("Should return label name list", () => { + completer.getCompletions(editor, session, { row: 0, column: 10 }, 'j', (s, res) => { + expect(res[0]).to.eql({caption: 'job', value: 'job', meta: 'label name'}); + }); + }); + + }); + + describe("When inside label matcher, and located at label name with __name__ match", () => { + sessionData = { + currentToken: { type: 'entity.name.tag', value: 'j', index: 5, start: 22 }, + tokens: [ + { type: 'paren.lparen', value: '{' }, + { type: 'entity.name.tag', value: '__name__' }, + { type: 'keyword.operator', value: '=~' }, + { type: 'string.quoted', value: '"node_cpu"' }, + { type: 'punctuation.operator', value: ',' }, + { type: 'entity.name.tag', value: 'j', 'index': 5, 'start': 22 }, + { type: 'paren.rparen', value: '}' } + ], + line: '{__name__=~"node_cpu",j}' + }; + + it("Should return label name list", () => { + completer.getCompletions(editor, session, { row: 0, column: 23 }, 'j', (s, res) => { + expect(res[0]).to.eql({caption: 'job', value: 'job', meta: 'label name'}); + }); + }); + + }); + + describe("When inside label matcher, and located at label value", () => { + sessionData = { + currentToken: { type: 'string.quoted', value: '"n"', index: 4, start: 13 }, + tokens: [ + { type: 'identifier', value: 'node_cpu' }, + { type: 'paren.lparen', value: '{' }, + { type: 'entity.name.tag', value: 'job' }, + { type: 'keyword.operator', value: '=' }, + { type: 'string.quoted', value: '"n"', index: 4, start: 13 }, + { type: 'paren.rparen', value: '}' } + ], + line: 'node_cpu{job="n"}' + }; + + it("Should return label value list", () => { + completer.getCompletions(editor, session, { row: 0, column: 15 }, 'n', (s, res) => { + expect(res[0]).to.eql({caption: 'node', value: 'node', meta: 'label value'}); + }); + }); + + }); + }); From 5bdd554671fc5e0aaf0486405d7607886b4ae875 Mon Sep 17 00:00:00 2001 From: Mitsuhiro Tanda Date: Wed, 13 Sep 2017 11:43:16 +0900 Subject: [PATCH 13/83] check args for query --- .../plugins/datasource/prometheus/specs/completer_specs.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/public/app/plugins/datasource/prometheus/specs/completer_specs.ts b/public/app/plugins/datasource/prometheus/specs/completer_specs.ts index 3d0371da7ed..c9fe4fa61c1 100644 --- a/public/app/plugins/datasource/prometheus/specs/completer_specs.ts +++ b/public/app/plugins/datasource/prometheus/specs/completer_specs.ts @@ -18,7 +18,7 @@ describe('Prometheus editor completer', function() { let editor = { session: session }; let datasourceStub = { - performInstantQuery: sinon.stub().returns(Promise.resolve( + performInstantQuery: sinon.stub().withArgs({ expr: '{__name__="node_cpu"' }).returns(Promise.resolve( [ { metric: { @@ -28,7 +28,7 @@ describe('Prometheus editor completer', function() { } ] )), - performSuggestQuery: sinon.stub().returns(Promise.resolve( + performSuggestQuery: sinon.stub().withArgs('node', true).returns(Promise.resolve( [ 'node_cpu' ] From bb8849785a7c6f27759a26d9dbb1ac1ce2a5869b Mon Sep 17 00:00:00 2001 From: Alin Sinpalean Date: Wed, 27 Sep 2017 14:20:51 +0200 Subject: [PATCH 14/83] Have include intervalFactor in its calculation, so always equal to the step query parameter. --- .../datasource/prometheus/datasource.ts | 6 +- .../prometheus/specs/datasource_specs.ts | 69 ++++++++++++++++--- 2 files changed, 64 insertions(+), 11 deletions(-) diff --git a/public/app/plugins/datasource/prometheus/datasource.ts b/public/app/plugins/datasource/prometheus/datasource.ts index 85139336dd4..c0bb3079290 100644 --- a/public/app/plugins/datasource/prometheus/datasource.ts +++ b/public/app/plugins/datasource/prometheus/datasource.ts @@ -104,7 +104,7 @@ export class PrometheusDatasource { var minInterval = this.intervalSeconds(this.templateSrv.replace(target.interval, options.scopedVars) || options.interval); var intervalFactor = target.intervalFactor || 1; var range = Math.ceil(end - start); - // Adjust the interval to take into account any specified minimum plus Prometheus limitations + // Adjust the interval to take into account any specified minimum and interval factor plus Prometheus limits var adjustedInterval = this.adjustInterval(interval, minInterval, range, intervalFactor); var scopedVars = options.scopedVars; @@ -116,7 +116,7 @@ export class PrometheusDatasource { "__interval_ms": {text: interval * 1000, value: interval * 1000}, }); } - target.step = query.step = interval * intervalFactor; + target.step = query.step = interval; // Only replace vars in expression after having (possibly) updated interval vars query.expr = this.templateSrv.replace(target.expr, scopedVars, self.interpolateQueryExpr); @@ -163,12 +163,12 @@ export class PrometheusDatasource { } adjustInterval(interval, minInterval, range, intervalFactor) { - interval = Math.max(interval, minInterval); // Prometheus will drop queries that might return more than 11000 data points. // Calibrate interval if it is too small. if (interval !== 0 && range / intervalFactor / interval > 11000) { interval = Math.ceil(range / intervalFactor / 11000); } + interval = Math.max(interval * intervalFactor, minInterval); return interval; } diff --git a/public/app/plugins/datasource/prometheus/specs/datasource_specs.ts b/public/app/plugins/datasource/prometheus/specs/datasource_specs.ts index 0ee59ef1923..a6e7f480f88 100644 --- a/public/app/plugins/datasource/prometheus/specs/datasource_specs.ts +++ b/public/app/plugins/datasource/prometheus/specs/datasource_specs.ts @@ -284,6 +284,7 @@ describe('PrometheusDatasource', function() { it('should be min interval when greater than auto interval', function() { var query = { + // 6 hour range range: { from: moment(1443438674760), to: moment(1443460274760) }, targets: [{ expr: 'test', @@ -299,6 +300,7 @@ describe('PrometheusDatasource', function() { }); it('should be auto interval when greater than min interval', function() { var query = { + // 6 hour range range: { from: moment(1443438674760), to: moment(1443460274760) }, targets: [{ expr: 'test', @@ -314,6 +316,7 @@ describe('PrometheusDatasource', function() { }); it('should result in querying fewer than 11000 data points', function() { var query = { + // 6 hour range range: { from: moment(1443438674760), to: moment(1443460274760) }, targets: [{ expr: 'test' }], interval: '1s' @@ -324,8 +327,9 @@ describe('PrometheusDatasource', function() { ctx.ds.query(query); ctx.$httpBackend.verifyNoOutstandingExpectation(); }); - it('should apply intervalFactor to min interval when greater', function() { + it('should not apply min interval when interval * intervalFactor greater', function() { var query = { + // 6 hour range range: { from: moment(1443438674760), to: moment(1443460274760) }, targets: [{ expr: 'test', @@ -335,13 +339,31 @@ describe('PrometheusDatasource', function() { interval: '5s' }; var urlExpected = 'proxied/api/v1/query_range?query=test' + - '&start=1443438675&end=1443460275&step=100'; + '&start=1443438675&end=1443460275&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 hour range + range: { from: moment(1443438674760), to: moment(1443460274760) }, + targets: [{ + expr: 'test', + interval: '15s', + intervalFactor: 2 + }], + interval: '5s' + }; + var urlExpected = 'proxied/api/v1/query_range?query=test' + + '&start=1443438675&end=1443460275&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 hour range range: { from: moment(1443438674760), to: moment(1443460274760) }, targets: [{ expr: 'test', @@ -404,6 +426,7 @@ describe('PrometheusDatasource', function() { it('should be unchanged when auto interval is greater than min interval', function() { var query = { + // 6 hour range range: { from: moment(1443438674760), to: moment(1443460274760) }, targets: [{ expr: 'rate(test[$__interval])', @@ -429,6 +452,7 @@ describe('PrometheusDatasource', function() { }); it('should be min interval when it is greater than auto interval', function() { var query = { + // 6 hour range range: { from: moment(1443438674760), to: moment(1443460274760) }, targets: [{ expr: 'rate(test[$__interval])', @@ -452,8 +476,9 @@ describe('PrometheusDatasource', function() { expect(query.scopedVars.__interval_ms.text).to.be(5 * 1000); expect(query.scopedVars.__interval_ms.value).to.be(5 * 1000); }); - it('should ignore intervalFactor', function() { + it('should account for intervalFactor', function() { var query = { + // 6 hour range range: { from: moment(1443438674760), to: moment(1443460274760) }, targets: [{ expr: 'rate(test[$__interval])', @@ -467,7 +492,7 @@ describe('PrometheusDatasource', function() { } }; var urlExpected = 'proxied/api/v1/query_range?query=' + - encodeURIComponent('rate(test[10s])') + + encodeURIComponent('rate(test[100s])') + '&start=1443438675&end=1443460275&step=100'; ctx.$httpBackend.expect('GET', urlExpected).respond(response); ctx.ds.query(query); @@ -478,8 +503,9 @@ describe('PrometheusDatasource', function() { expect(query.scopedVars.__interval_ms.text).to.be(10 * 1000); expect(query.scopedVars.__interval_ms.value).to.be(10 * 1000); }); - it('should ignore intervalFactor', function() { + it('should be interval * intervalFactor when greater than min interval', function() { var query = { + // 6 hour range range: { from: moment(1443438674760), to: moment(1443460274760) }, targets: [{ expr: 'rate(test[$__interval])', @@ -493,8 +519,35 @@ describe('PrometheusDatasource', function() { } }; var urlExpected = 'proxied/api/v1/query_range?query=' + - encodeURIComponent('rate(test[10s])') + - '&start=1443438675&end=1443460275&step=100'; + encodeURIComponent('rate(test[50s])') + + '&start=1443438675&end=1443460275&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 hour range + range: { from: moment(1443438674760), to: moment(1443460274760) }, + 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=1443438675&end=1443460275&step=15'; ctx.$httpBackend.expect('GET', urlExpected).respond(response); ctx.ds.query(query); ctx.$httpBackend.verifyNoOutstandingExpectation(); @@ -519,7 +572,7 @@ describe('PrometheusDatasource', function() { } }; var urlExpected = 'proxied/api/v1/query_range?query=' + - encodeURIComponent('rate(test[6s])') + + encodeURIComponent('rate(test[60s])') + '&start=1443438675&end=1444043475&step=60'; ctx.$httpBackend.expect('GET', urlExpected).respond(response); ctx.ds.query(query); From 00726e93eb5021361be3dea5b593e53cfa4b062c Mon Sep 17 00:00:00 2001 From: Matthew McGinn Date: Sat, 30 Sep 2017 19:26:19 -0400 Subject: [PATCH 15/83] adding support for token-based slack file.upload API call for posting images to slack --- docs/sources/alerting/notifications.md | 5 +- pkg/services/alerting/notifiers/slack.go | 86 +++++++++++++++++- pkg/services/alerting/notifiers/slack_test.go | 3 + public/img/mixed_styles.png | Bin 0 -> 29916 bytes 4 files changed, 89 insertions(+), 5 deletions(-) create mode 100644 public/img/mixed_styles.png diff --git a/docs/sources/alerting/notifications.md b/docs/sources/alerting/notifications.md index 359315650d9..de9e5abd472 100644 --- a/docs/sources/alerting/notifications.md +++ b/docs/sources/alerting/notifications.md @@ -48,12 +48,15 @@ external image destination if available or fallback to attaching the image in th To set up slack you need to configure an incoming webhook url at slack. You can follow their guide for how to do that https://api.slack.com/incoming-webhooks If you want to include screenshots of the firing alerts -in the slack messages you have to configure the [external image destination](#external-image-store) in Grafana. +in the slack messages you have to configure either the [external image destination](#external-image-store) in Grafana, +or a bot integration via Slack Apps. Follow Slack's guide to set up a bot integration and use the token provided +https://api.slack.com/bot-users, which starts with "xoxb". Setting | Description ---------- | ----------- Recipient | allows you to override the slack recipient. Mention | make it possible to include a mention in the slack notification sent by Grafana. Ex @here or @channel +Token | If provided, Grafana will upload the generated image via Slack's file.upload API method, not the external image destination. ### PagerDuty diff --git a/pkg/services/alerting/notifiers/slack.go b/pkg/services/alerting/notifiers/slack.go index d917daa3620..1ee16453a5f 100644 --- a/pkg/services/alerting/notifiers/slack.go +++ b/pkg/services/alerting/notifiers/slack.go @@ -1,7 +1,11 @@ package notifiers import ( + "bytes" "encoding/json" + "io" + "mime/multipart" + "os" "time" "github.com/grafana/grafana/pkg/bus" @@ -15,7 +19,7 @@ func init() { alerting.RegisterNotifier(&alerting.NotifierPlugin{ Type: "slack", Name: "Slack", - Description: "Sends notifications using Grafana server configured STMP settings", + Description: "Sends notifications to Slack via Slack Webhooks", Factory: NewSlackNotifier, OptionsTemplate: `

Slack settings

@@ -45,6 +49,17 @@ func init() { Mention a user or a group using @ when notifying in a channel +
+ Token + + + + Provide a bot token to use the Slack file.upload API (starts with "xoxb") + +
`, }) @@ -58,12 +73,14 @@ func NewSlackNotifier(model *m.AlertNotification) (alerting.Notifier, error) { recipient := model.Settings.Get("recipient").MustString() mention := model.Settings.Get("mention").MustString() + token := model.Settings.Get("token").MustString() return &SlackNotifier{ NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.Settings), Url: url, Recipient: recipient, Mention: mention, + Token: token, log: log.New("alerting.notifier.slack"), }, nil } @@ -73,6 +90,7 @@ type SlackNotifier struct { Url string Recipient string Mention string + Token string log log.Logger } @@ -110,6 +128,11 @@ func (this *SlackNotifier) Notify(evalContext *alerting.EvalContext) error { if evalContext.Rule.State != m.AlertStateOK { //dont add message when going back to alert state ok. message += " " + evalContext.Rule.Message } + image_url := "" + // default to file.upload API method if a token is provided + if this.Token == "" { + image_url = evalContext.ImagePublicUrl + } body := map[string]interface{}{ "attachments": []map[string]interface{}{ @@ -120,7 +143,7 @@ func (this *SlackNotifier) Notify(evalContext *alerting.EvalContext) error { "title_link": ruleUrl, "text": message, "fields": fields, - "image_url": evalContext.ImagePublicUrl, + "image_url": image_url, "footer": "Grafana v" + setting.BuildVersion, "footer_icon": "https://grafana.com/assets/img/fav32.png", "ts": time.Now().Unix(), @@ -133,14 +156,69 @@ func (this *SlackNotifier) Notify(evalContext *alerting.EvalContext) error { if this.Recipient != "" { body["channel"] = this.Recipient } - data, _ := json.Marshal(&body) cmd := &m.SendWebhookSync{Url: this.Url, Body: string(data)} - if err := bus.DispatchCtx(evalContext.Ctx, cmd); err != nil { this.log.Error("Failed to send slack notification", "error", err, "webhook", this.Name) return err } + if this.Token != "" { + slackUploadUrl := "https://slack.com/api/files.upload" + if evalContext.ImageOnDiskPath == "" { + evalContext.ImageOnDiskPath = "public/img/mixed_styles.png" + } + this.log.Info("Uploading to slack via file.upload API") + headers, uploadBody, err := GenerateSlackUpload(evalContext.ImageOnDiskPath, this.Token, this.Recipient) + if err != nil { + return err + } + cmd := &m.SendWebhookSync{Url: slackUploadUrl, Body: uploadBody.String(), HttpHeader: headers, HttpMethod: "POST"} + if err := bus.DispatchCtx(evalContext.Ctx, cmd); err != nil { + this.log.Error("Failed to upload slack image", "error", err, "webhook", "file.upload") + return err + } + if err != nil { + return err + } + } return nil } + +func GenerateSlackUpload(file string, token string, recipient string) (map[string]string, bytes.Buffer, error) { + // Slack requires all POSTs to files.upload to present + // an "application/x-www-form-urlencoded" encoded querystring + // See https://api.slack.com/methods/files.upload + var b bytes.Buffer + w := multipart.NewWriter(&b) + // Add the generated image file + f, err := os.Open(file) + if err != nil { + return nil, b, err + } + defer f.Close() + fw, err := w.CreateFormFile("file", file) + if err != nil { + return nil, b, err + } + _, err = io.Copy(fw, f) + if err != nil { + return nil, b, err + } + // Add the authorization token + err = w.WriteField("token", token) + if err != nil { + return nil, b, err + } + // Add the channel(s) to POST to + err = w.WriteField("channels", recipient) + if err != nil { + return nil, b, err + } + w.Close() + headers := map[string]string{ + "Content-Type": w.FormDataContentType(), + "Authorization": "auth_token=\"" + token + "\"", + } + return headers, b, nil +} diff --git a/pkg/services/alerting/notifiers/slack_test.go b/pkg/services/alerting/notifiers/slack_test.go index 5b1763064aa..6f5b69fcb0a 100644 --- a/pkg/services/alerting/notifiers/slack_test.go +++ b/pkg/services/alerting/notifiers/slack_test.go @@ -48,6 +48,7 @@ func TestSlackNotifier(t *testing.T) { So(slackNotifier.Url, ShouldEqual, "http://google.com") So(slackNotifier.Recipient, ShouldEqual, "") So(slackNotifier.Mention, ShouldEqual, "") + So(slackNotifier.Token, ShouldEqual, "") }) Convey("from settings with Recipient and Mention", func() { @@ -56,6 +57,7 @@ func TestSlackNotifier(t *testing.T) { "url": "http://google.com", "recipient": "#ds-opentsdb", "mention": "@carl" + "token": "xoxb-XXXXXXXX-XXXXXXXX-XXXXXXXXXX", }` settingsJSON, _ := simplejson.NewJson([]byte(json)) @@ -74,6 +76,7 @@ func TestSlackNotifier(t *testing.T) { So(slackNotifier.Url, ShouldEqual, "http://google.com") So(slackNotifier.Recipient, ShouldEqual, "#ds-opentsdb") So(slackNotifier.Mention, ShouldEqual, "@carl") + So(slackNotifier.Token, ShouldEqual, "xoxb-XXXXXXXX-XXXXXXXX-XXXXXXXXXX") }) }) diff --git a/public/img/mixed_styles.png b/public/img/mixed_styles.png new file mode 100644 index 0000000000000000000000000000000000000000..042d95c09b50fdb5c10a393fa68a7dcfb69f9254 GIT binary patch literal 29916 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 LnmkYQBof0EczP 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 Date: Sun, 1 Oct 2017 10:37:59 -0400 Subject: [PATCH 16/83] fixing json for CI --- pkg/services/alerting/notifiers/slack_test.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pkg/services/alerting/notifiers/slack_test.go b/pkg/services/alerting/notifiers/slack_test.go index 6f5b69fcb0a..13f8c7b48b7 100644 --- a/pkg/services/alerting/notifiers/slack_test.go +++ b/pkg/services/alerting/notifiers/slack_test.go @@ -51,13 +51,13 @@ func TestSlackNotifier(t *testing.T) { So(slackNotifier.Token, ShouldEqual, "") }) - Convey("from settings with Recipient and Mention", func() { + Convey("from settings with Recipient, Mention, and Token", func() { json := ` { "url": "http://google.com", "recipient": "#ds-opentsdb", - "mention": "@carl" - "token": "xoxb-XXXXXXXX-XXXXXXXX-XXXXXXXXXX", + "mention": "@carl", + "token": "xoxb-XXXXXXXX-XXXXXXXX-XXXXXXXXXX" }` settingsJSON, _ := simplejson.NewJson([]byte(json)) From c338d8250bc6d561c2dcece12b7b2242fcd03443 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 2 Oct 2017 13:15:28 +0200 Subject: [PATCH 17/83] build: minor webpack fix --- package.json | 12 ++++++------ scripts/webpack/webpack.dev.js | 8 ++++++++ 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index f742f81eba7..cc28da6346e 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,9 @@ "@types/node": "^8.0.31", "@types/react": "^16.0.5", "@types/react-dom": "^15.5.4", + "angular-mocks": "^1.6.6", "autoprefixer": "^6.4.0", + "awesome-typescript-loader": "^3.2.3", "babel-core": "^6.26.0", "babel-loader": "^7.1.2", "babel-preset-es2015": "^6.24.1", @@ -66,6 +68,7 @@ "ng-annotate-loader": "^0.6.1", "ng-annotate-webpack-plugin": "^0.2.1-pre", "ngtemplate-loader": "^2.0.1", + "npm": "^5.4.2", "phantomjs-prebuilt": "^2.1.15", "postcss-browser-reporter": "^0.5.0", "postcss-loader": "^2.0.6", @@ -85,10 +88,7 @@ "webpack-bundle-analyzer": "^2.9.0", "webpack-cleanup-plugin": "^0.5.1", "webpack-merge": "^4.1.0", - "zone.js": "^0.7.2", - "awesome-typescript-loader": "^3.2.3", - "angular-mocks": "^1.6.6", - "npm": "^5.4.2" + "zone.js": "^0.7.2" }, "scripts": { "dev": "./node_modules/.bin/webpack --progress --colors --config scripts/webpack/webpack.dev.js", @@ -99,17 +99,17 @@ }, "license": "Apache-2.0", "dependencies": { - "babel-polyfill": "^6.26.0", - "jquery": "^3.2.1", "angular": "^1.6.6", "angular-bindonce": "^0.3.1", "angular-native-dragdrop": "^1.2.2", "angular-route": "^1.6.6", "angular-sanitize": "^1.6.6", + "babel-polyfill": "^6.26.0", "brace": "^0.10.0", "clipboard": "^1.7.1", "eventemitter3": "^2.0.3", "file-saver": "^1.3.3", + "jquery": "^3.2.1", "lodash": "^4.17.4", "moment": "^2.18.1", "mousetrap": "^1.6.0", diff --git a/scripts/webpack/webpack.dev.js b/scripts/webpack/webpack.dev.js index 126835a4745..98ecb44e284 100644 --- a/scripts/webpack/webpack.dev.js +++ b/scripts/webpack/webpack.dev.js @@ -9,7 +9,15 @@ const ExtractTextPlugin = require("extract-text-webpack-plugin"); const WebpackCleanupPlugin = require('webpack-cleanup-plugin'); const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin; const pkg = require('../../package.json'); +const _ = require('lodash'); + let dependencies = Object.keys(pkg.dependencies); +// remove jquery +dependencies = _.filter(dependencies, function(key) { + return key !== 'jquery'; +}); +// add it first +dependencies.unshift('jquery'); module.exports = merge(common, { devtool: "source-map", From 03a88206a3f28a3588145487e43f181b5297870e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 2 Oct 2017 15:30:14 +0200 Subject: [PATCH 18/83] Merge branch 'master' of github.com:grafana/grafana --- .../features/dashboard/partials/settings.html | 6 +- public/app/features/dashlinks/editor.html | 100 +++++++++--------- public/app/features/plugins/plugin_loader.ts | 26 +++-- 3 files changed, 72 insertions(+), 60 deletions(-) diff --git a/public/app/features/dashboard/partials/settings.html b/public/app/features/dashboard/partials/settings.html index b8d8303a51b..e9f30c81b55 100644 --- a/public/app/features/dashboard/partials/settings.html +++ b/public/app/features/dashboard/partials/settings.html @@ -93,13 +93,13 @@
- - -
diff --git a/public/app/features/dashlinks/editor.html b/public/app/features/dashlinks/editor.html index f52835aa5f9..d1d73520283 100644 --- a/public/app/features/dashlinks/editor.html +++ b/public/app/features/dashlinks/editor.html @@ -3,74 +3,78 @@
-
-
+
+
- Type + Type
-
- With tags - + With tags +
- -
- -
- -
-
  • Url
  • - -
    - -
    - -
    -
    - -
    -
    - -
    -
    - -
    - Title - -
    - -
    -
    - Title + +
    + Title
    +
    +
    +
  • Url
  • + +
    -
    - Tooltip - -
    +
    + Title + +
    -
    - Icon -
    - +
    + Tooltip + +
    + +
    + Icon +
    + +
    -
    +
    +
    +
    + Include +
    +
    +
    + + + +
    +
    + +
    - Include - - - + +
    +
    + +
    +
    +
    + + diff --git a/public/app/features/plugins/plugin_loader.ts b/public/app/features/plugins/plugin_loader.ts index cfb4975a950..ac6fc06ce28 100644 --- a/public/app/features/plugins/plugin_loader.ts +++ b/public/app/features/plugins/plugin_loader.ts @@ -7,6 +7,7 @@ import angular from 'angular'; import jquery from 'jquery'; import config from 'app/core/config'; import TimeSeries from 'app/core/time_series2'; +import * as datemath from 'app/core/utils/datemath'; import * as graphitePlugin from 'app/plugins/datasource/graphite/module'; import * as cloudwatchPlugin from 'app/plugins/datasource/cloudwatch/module'; @@ -78,15 +79,22 @@ System.locate = function(load) { }); }; -System.registerDynamic('lodash', [], true, function(require, exports, module) { module.exports = _; }); -System.registerDynamic('moment', [], true, function(require, exports, module) { module.exports = moment; }); -System.registerDynamic('jquery', [], true, function(require, exports, module) { module.exports = jquery; }); -System.registerDynamic('angular', [], true, function(require, exports, module) { module.exports = angular; }); -System.registerDynamic('app/plugins/sdk', [], true, function(require, exports, module) { module.exports = sdk; }); -System.registerDynamic('app/core/utils/kbn', [], true, function(require, exports, module) { module.exports = kbn; }); -System.registerDynamic('app/core/config', [], true, function(require, exports, module) { module.exports = config; }); -System.registerDynamic('app/core/time_series', [], true, function(require, exports, module) { module.exports = TimeSeries; }); -System.registerDynamic('app/core/time_series2', [], true, function(require, exports, module) { module.exports = TimeSeries; }); +function exposeToPlugin(name: string, component: any) { + System.registerDynamic(name, [], true, function(require, exports, module) { + module.exports = component; + }); +} + +exposeToPlugin('lodash', _); +exposeToPlugin('moment', moment); +exposeToPlugin('jquery', jquery); +exposeToPlugin('angular', angular); +exposeToPlugin('app/plugins/sdk', sdk); +exposeToPlugin('app/core/utils/datemath', datemath); +exposeToPlugin('app/core/utils/kbn', kbn); +exposeToPlugin('app/core/config', config); +exposeToPlugin('app/core/time_series', TimeSeries); +exposeToPlugin('app/core/time_series2', TimeSeries); import 'vendor/flot/jquery.flot'; import 'vendor/flot/jquery.flot.selection'; From 503336bcd5c1d48f102093e34a9b2eaec08b05e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 2 Oct 2017 15:30:37 +0200 Subject: [PATCH 19/83] docs: minor docs fix --- docs/sources/features/panels/singlestat.md | 27 ---------------------- 1 file changed, 27 deletions(-) diff --git a/docs/sources/features/panels/singlestat.md b/docs/sources/features/panels/singlestat.md index cf04419dbc5..5891848f579 100644 --- a/docs/sources/features/panels/singlestat.md +++ b/docs/sources/features/panels/singlestat.md @@ -64,7 +64,6 @@ Sparklines are a great way of seeing the historical data related to the summary > ***Pro-tip:*** Reduce the opacity on fill colors for nice looking panels. -<<<<<<< HEAD ### Gauge Gauges gives a clear picture of how high a value is in it's context. It's a great way to see if a value is close to the thresholds. The gauge uses the colors set in the color options. @@ -78,39 +77,13 @@ Gauges gives a clear picture of how high a value is in it's context. It's a grea
    -||||||| merged common ancestors -======= -### Gauge - -Gauges gives a clear picture of how high a value is in it's context. It's a great way to see if a value is close to the thresholds. The gauge uses the colors set in the color options. - -{{< docs-imagebox img="/img/docs/v45/singlestat-gauge-options.png" max-width="500px" class="docs-image--right docs-image--no-shadow">}} - -1. `Show`: The show checkbox will toggle wether the gauge is shown in the panel. When unselected, only the Singlestat value will appear. -2. `Min/Max`: This sets the start and end point for the gauge. -3. `Threshold Labels`: Check if you want to show the threshold labels. Thresholds are set in the color options. -4. `Threshold Markers`: Check if you want to have a second meter showing the thresholds. - ->>>>>>> 0a65100eaf64cd57b38110001bf614630821610c ### Value to text mapping -<<<<<<< HEAD {{< docs-imagebox img="/img/docs/v45/singlestat-value-mapping.png" class="docs-image--right docs-image--no-shadow">}} Value to text mapping allows you to translate the value of the summary stat into explicit text. The text will respect all styling, thresholds and customization defined for the value. This can be useful to translate the number of the main Singlestat value into a context-specific human-readable word or message. -||||||| merged common ancestors -Value to text mapping allows you to translate the value of the summary stat into explicit text. The text will respect all styling, thresholds and customization defined for the value. This can be useful to translate the number of the main Singlestat value into a context-specific human-readable word or message. -======= -{{< docs-imagebox img="/img/docs/v45/singlestat-value-mapping.png" class="docs-image--right docs-image--no-shadow">}} ->>>>>>> 0a65100eaf64cd57b38110001bf614630821610c -<<<<<<< HEAD
    -||||||| merged common ancestors - -======= -Value to text mapping allows you to translate the value of the summary stat into explicit text. The text will respect all styling, thresholds and customization defined for the value. This can be useful to translate the number of the main Singlestat value into a context-specific human-readable word or message. ->>>>>>> 0a65100eaf64cd57b38110001bf614630821610c ## Troubleshooting From d8f6c73aabdacc9db959cbcf12f01720fb66e62f Mon Sep 17 00:00:00 2001 From: cglrkn Date: Mon, 2 Oct 2017 16:42:09 +0300 Subject: [PATCH 20/83] Update script to make it use OpsGenie's REST API The script is making API calls to the deprecated OpsGenie Web API, we updated the script to make it use new OpsGenie's REST API. --- pkg/services/alerting/notifiers/opsgenie.go | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/pkg/services/alerting/notifiers/opsgenie.go b/pkg/services/alerting/notifiers/opsgenie.go index e67ccfb10e5..5af9fa69611 100644 --- a/pkg/services/alerting/notifiers/opsgenie.go +++ b/pkg/services/alerting/notifiers/opsgenie.go @@ -37,8 +37,7 @@ func init() { } var ( - opsgenieCreateAlertURL string = "https://api.opsgenie.com/v1/json/alert" - opsgenieCloseAlertURL string = "https://api.opsgenie.com/v1/json/alert/close" + opsgenieAlertURL string = "https://api.opsgenie.com/v2/alerts" ) func NewOpsGenieNotifier(model *m.AlertNotification) (alerting.Notifier, error) { @@ -87,7 +86,6 @@ func (this *OpsGenieNotifier) createAlert(evalContext *alerting.EvalContext) err } bodyJSON := simplejson.New() - bodyJSON.Set("apiKey", this.ApiKey) bodyJSON.Set("message", evalContext.Rule.Name) bodyJSON.Set("source", "Grafana") bodyJSON.Set("alias", "alertId-"+strconv.FormatInt(evalContext.Rule.Id, 10)) @@ -103,9 +101,13 @@ func (this *OpsGenieNotifier) createAlert(evalContext *alerting.EvalContext) err body, _ := bodyJSON.MarshalJSON() cmd := &m.SendWebhookSync{ - Url: opsgenieCreateAlertURL, + Url: opsgenieAlertURL, Body: string(body), HttpMethod: "POST", + HttpHeader: map[string]string{ + "Content-Type": "application/json", + "Authorization": fmt.Sprintf("GenieKey %s", this.ApiKey), + }, } if err := bus.DispatchCtx(evalContext.Ctx, cmd); err != nil { @@ -119,14 +121,17 @@ func (this *OpsGenieNotifier) closeAlert(evalContext *alerting.EvalContext) erro this.log.Info("Closing OpsGenie alert", "ruleId", evalContext.Rule.Id, "notification", this.Name) bodyJSON := simplejson.New() - bodyJSON.Set("apiKey", this.ApiKey) - bodyJSON.Set("alias", "alertId-"+strconv.FormatInt(evalContext.Rule.Id, 10)) + bodyJSON.Set("source", "Grafana") body, _ := bodyJSON.MarshalJSON() cmd := &m.SendWebhookSync{ - Url: opsgenieCloseAlertURL, + Url: fmt.Sprintf("%s/%s/close?identifierType=alias", opsgenieAlertURL,"alertId-"+strconv.FormatInt(evalContext.Rule.Id, 10)), Body: string(body), HttpMethod: "POST", + HttpHeader: map[string]string{ + "Content-Type": "application/json", + "Authorization": fmt.Sprintf("GenieKey %s", this.ApiKey), + }, } if err := bus.DispatchCtx(evalContext.Ctx, cmd); err != nil { From a02cac21268b2fc1184b549f58fe0b75bbe089d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 2 Oct 2017 16:45:25 +0200 Subject: [PATCH 21/83] refactoring: minor refactoring of PR #8916 --- CHANGELOG.md | 7 ++++--- public/app/features/dashboard/time_srv.ts | 3 ++- public/app/features/panel/metrics_panel_ctrl.ts | 4 +--- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2deafc59207..23633bfbc1e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ * **Prometheus**: Add support for instant queries [#5765](https://github.com/grafana/grafana/issues/5765), thx [@mtanda](https://github.com/mtanda) * **Cloudwatch**: Add support for alerting using the cloudwatch datasource [#8050](https://github.com/grafana/grafana/pull/8050), thx [@mtanda](https://github.com/mtanda) * **Pagerduty**: Include triggering series in pagerduty notification [#8479](https://github.com/grafana/grafana/issues/8479), thx [@rickymoorhouse](https://github.com/rickymoorhouse) +* **Timezone**: Time ranges like Today & Yesterday now work correctly when timezone setting is set to UTC [#8916](https://github.com/grafana/grafana/issues/8916), thx [@ctide](https://github.com/ctide) ## Minor * **SMTP**: Make it possible to set specific EHLO for smtp client. [#9319](https://github.com/grafana/grafana/issues/9319) @@ -31,7 +32,7 @@ # 4.5.2 (2017-09-22) -## Fixes +## Fixes * **Graphite**: Fix for issues with jsonData & graphiteVersion null errors [#9258](https://github.com/grafana/grafana/issues/9258) * **Graphite**: Fix for Grafana internal metrics to Graphite sending NaN values [#9279](https://github.com/grafana/grafana/issues/9279) * **HTTP API**: Fix for HEAD method requests [#9307](https://github.com/grafana/grafana/issues/9307) @@ -44,7 +45,7 @@ * **MySQL**: Fixed issue with query editor not showing [#9247](https://github.com/grafana/grafana/issues/9247) ## Breaking changes -* **Metrics**: The metric structure for internal metrics about Grafana published to graphite has changed. This might break dashboards for internal metrics. +* **Metrics**: The metric structure for internal metrics about Grafana published to graphite has changed. This might break dashboards for internal metrics. # 4.5.0 (2017-09-14) @@ -73,7 +74,7 @@ ### Breaking change * **InfluxDB/Elasticsearch**: The panel & data source option named "Group by time interval" is now named "Min time interval" and does now always define a lower limit for the auto group by time. Without having to use `>` prefix (that prefix still works). This should in theory have close to zero actual impact on existing dashboards. It does mean that if you used this setting to define a hard group by time interval of, say "1d", if you zoomed to a time range wide enough the time range could increase above the "1d" range as the setting is now always considered a lower limit. -* **Elasticsearch**: Elasticsearch metric queries without date histogram now return table formated data making table panel much easier to use for this use case. Should not break/change existing dashboards with stock panels but external panel plugins can be affected. +* **Elasticsearch**: Elasticsearch metric queries without date histogram now return table formated data making table panel much easier to use for this use case. Should not break/change existing dashboards with stock panels but external panel plugins can be affected. ## Changes diff --git a/public/app/features/dashboard/time_srv.ts b/public/app/features/dashboard/time_srv.ts index eb4bddb9e80..dfed156afae 100644 --- a/public/app/features/dashboard/time_srv.ts +++ b/public/app/features/dashboard/time_srv.ts @@ -195,7 +195,8 @@ class TimeSrv { from: moment.isMoment(this.time.from) ? moment(this.time.from) : this.time.from, to: moment.isMoment(this.time.to) ? moment(this.time.to) : this.time.to, }; - var timezone = this.dashboard && this.dashboard.getTimezone ? this.dashboard.getTimezone() : 'local'; + + var timezone = this.dashboard && this.dashboard.getTimezone(); return { from: dateMath.parse(raw.from, false, timezone), diff --git a/public/app/features/panel/metrics_panel_ctrl.ts b/public/app/features/panel/metrics_panel_ctrl.ts index 1d055c11da0..38d9c631b54 100644 --- a/public/app/features/panel/metrics_panel_ctrl.ts +++ b/public/app/features/panel/metrics_panel_ctrl.ts @@ -217,10 +217,8 @@ class MetricsPanelCtrl extends PanelCtrl { "__interval_ms": {text: this.intervalMs, value: this.intervalMs}, }); - var timezone = this.dashboard.getTimezone ? this.dashboard.getTimezone() : 'local'; - var metricsQuery = { - timezone: timezone, + timezone: this.dashboard.getTimezone(), panelId: this.panel.id, range: this.range, rangeRaw: this.range.raw, From 4cc878b56de3133059400743e43bb7130c3a2159 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 2 Oct 2017 17:10:23 +0200 Subject: [PATCH 22/83] build: fixed build --- package.json | 4 + .../dashboard/specs/time_srv_specs.ts | 3 +- yarn.lock | 260 +++++++++++++++++- 3 files changed, 258 insertions(+), 9 deletions(-) diff --git a/package.json b/package.json index cc28da6346e..1bbe44991b9 100644 --- a/package.json +++ b/package.json @@ -52,6 +52,7 @@ "grunt-webpack": "^3.0.2", "html-loader": "^0.5.1", "html-webpack-plugin": "^2.30.1", + "husky": "^0.14.3", "jshint-stylish": "~2.2.1", "json-loader": "^0.5.7", "karma": "1.7.0", @@ -63,6 +64,7 @@ "karma-sinon": "^1.0.5", "karma-sourcemap-loader": "^0.3.7", "karma-webpack": "^2.0.4", + "lint-staged": "^4.2.3", "load-grunt-tasks": "3.5.2", "mocha": "3.5.0", "ng-annotate-loader": "^0.6.1", @@ -73,6 +75,7 @@ "postcss-browser-reporter": "^0.5.0", "postcss-loader": "^2.0.6", "postcss-reporter": "^5.0.0", + "prettier": "1.7.3", "react-test-renderer": "^16.0.0", "rxjs": "^5.4.3", "sass-lint": "^1.10.2", @@ -95,6 +98,7 @@ "watch": "./node_modules/.bin/webpack --progress --colors --watch --config scripts/webpack/webpack.dev.js", "build": "./node_modules/grunt-cli/bin/grunt build", "test": "./node_modules/grunt-cli/bin/grunt test", + "lint" : "node ./node_modules/tslint/lib/tslint-cli.js -c tslint.json --project ./tsconfig.json --type-check", "watch-test": "./node_modules/grunt-cli/bin/grunt karma:dev" }, "license": "Apache-2.0", diff --git a/public/app/features/dashboard/specs/time_srv_specs.ts b/public/app/features/dashboard/specs/time_srv_specs.ts index 3c0f2637f0e..474297597c9 100644 --- a/public/app/features/dashboard/specs/time_srv_specs.ts +++ b/public/app/features/dashboard/specs/time_srv_specs.ts @@ -1,4 +1,4 @@ -import {describe, beforeEach, it, expect, angularMocks} from 'test/lib/common'; +import {describe, beforeEach, it, expect, sinon, angularMocks} from 'test/lib/common'; import helpers from 'test/specs/helpers'; import '../time_srv'; @@ -8,6 +8,7 @@ describe('timeSrv', function() { var ctx = new helpers.ServiceTestContext(); var _dashboard: any = { time: {from: 'now-6h', to: 'now'}, + getTimezone: sinon.stub().returns('browser') }; beforeEach(angularMocks.module('grafana.core')); diff --git a/yarn.lock b/yarn.lock index c8ce8d4061c..32e25dd691e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -364,7 +364,7 @@ ansi-align@^2.0.0: dependencies: string-width "^2.0.0" -ansi-escapes@^1.1.0: +ansi-escapes@^1.0.0, ansi-escapes@^1.1.0: version "1.4.0" resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-1.4.0.tgz#d3a8a83b319aa67793662b13e761c7911422306e" @@ -380,7 +380,7 @@ ansi-styles@^2.2.1: version "2.2.1" resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-2.2.1.tgz#b432dd3358b634cf75e1e4664368240533c1ddbe" -ansi-styles@^3.1.0: +ansi-styles@^3.1.0, ansi-styles@^3.2.0: version "3.2.0" resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.0.tgz#c159b8d5be0f9e5a6f346dab94f16ce022161b88" dependencies: @@ -401,6 +401,10 @@ anymatch@^1.3.0: micromatch "^2.1.5" normalize-path "^2.0.0" +app-root-path@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/app-root-path/-/app-root-path-2.0.1.tgz#cd62dcf8e4fd5a417efc664d2e5b10653c651b46" + aproba@^1.0.3, aproba@^1.1.1, aproba@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/aproba/-/aproba-1.1.2.tgz#45c6629094de4e96f693ef7eab74ae079c240fc1" @@ -1474,6 +1478,10 @@ chownr@^1.0.1, chownr@~1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.0.1.tgz#e2a75042a9551908bebd25b8523d5f9769d79181" +ci-info@^1.0.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-1.1.1.tgz#47b44df118c48d2597b56d342e7e25791060171a" + cipher-base@^1.0.0, cipher-base@^1.0.1, cipher-base@^1.0.3: version "1.0.4" resolved "https://registry.yarnpkg.com/cipher-base/-/cipher-base-1.0.4.tgz#8760e4ecc272f4c363532f926d874aae2c1397de" @@ -1518,18 +1526,29 @@ cli-boxes@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/cli-boxes/-/cli-boxes-1.0.0.tgz#4fa917c3e59c94a004cd61f8ee509da651687143" -cli-cursor@^1.0.1: +cli-cursor@^1.0.1, cli-cursor@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-1.0.2.tgz#64da3f7d56a54412e59794bd62dc35295e8f2987" dependencies: restore-cursor "^1.0.1" +cli-spinners@^0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/cli-spinners/-/cli-spinners-0.1.2.tgz#bb764d88e185fb9e1e6a2a1f19772318f605e31c" + cli-table@~0.3.1: version "0.3.1" resolved "https://registry.yarnpkg.com/cli-table/-/cli-table-0.3.1.tgz#f53b05266a8b1a0b934b3d0821e6e2dc5914ae23" dependencies: colors "1.0.3" +cli-truncate@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/cli-truncate/-/cli-truncate-0.2.1.tgz#9f15cfbb0705005369216c626ac7d05ab90dd574" + dependencies: + slice-ansi "0.0.4" + string-width "^1.0.1" + cli-width@^2.0.0: version "2.2.0" resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-2.2.0.tgz#ff19ede8a9a5e579324147b0c11f0fbcbabed639" @@ -1843,6 +1862,19 @@ core-util-is@1.0.2, core-util-is@~1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" +cosmiconfig@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-1.1.0.tgz#0dea0f9804efdfb929fbb1b188e25553ea053d37" + dependencies: + graceful-fs "^4.1.2" + js-yaml "^3.4.3" + minimist "^1.2.0" + object-assign "^4.0.1" + os-homedir "^1.0.1" + parse-json "^2.2.0" + pinkie-promise "^2.0.0" + require-from-string "^1.1.0" + cosmiconfig@^2.1.0, cosmiconfig@^2.1.1: version "2.2.2" resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-2.2.2.tgz#6173cebd56fac042c1f4390edf7af6c07c7cb892" @@ -2069,6 +2101,10 @@ dashdash@^1.12.0: dependencies: assert-plus "^1.0.0" +date-fns@^1.27.2: + version "1.28.5" + resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-1.28.5.tgz#257cfc45d322df45ef5658665967ee841cd73faf" + date-now@^0.1.4: version "0.1.4" resolved "https://registry.yarnpkg.com/date-now/-/date-now-0.1.4.tgz#eaf439fd4d4848ad74e5cc7dbef200672b9e345b" @@ -2373,6 +2409,10 @@ electron-to-chromium@^1.2.7: version "1.3.24" resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.24.tgz#9b7b88bb05ceb9fa016a177833cc2dde388f21b6" +elegant-spinner@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/elegant-spinner/-/elegant-spinner-1.0.1.tgz#db043521c95d7e303fd8f345bedc3349cfb0729e" + elliptic@^6.0.0: version "6.4.0" resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.4.0.tgz#cac9af8762c85836187003c8dfe193e5e2eae5df" @@ -2764,6 +2804,18 @@ execa@^0.7.0: signal-exit "^3.0.0" strip-eof "^1.0.0" +execa@^0.8.0: + version "0.8.0" + resolved "https://registry.yarnpkg.com/execa/-/execa-0.8.0.tgz#d8d76bbc1b55217ed190fd6dd49d3c774ecfc8da" + dependencies: + cross-spawn "^5.0.1" + get-stream "^3.0.0" + is-stream "^1.1.0" + npm-run-path "^2.0.0" + p-finally "^1.0.0" + signal-exit "^3.0.0" + strip-eof "^1.0.0" + exit-hook@^1.0.0: version "1.1.1" resolved "https://registry.yarnpkg.com/exit-hook/-/exit-hook-1.1.1.tgz#f05ca233b48c05d54fff07765df8507e95c02ff8" @@ -2943,7 +2995,7 @@ fd-slicer@~1.0.1: dependencies: pend "~1.2.0" -figures@^1.0.1, figures@^1.3.5: +figures@^1.0.1, figures@^1.3.5, figures@^1.7.0: version "1.7.0" resolved "https://registry.yarnpkg.com/figures/-/figures-1.7.0.tgz#cbe1e3affcf1cd44b80cadfed28dc793a9701d2e" dependencies: @@ -3261,6 +3313,10 @@ get-caller-file@^1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-1.0.2.tgz#f702e63127e7e231c160a80c1554acb70d5047e5" +get-own-enumerable-property-symbols@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/get-own-enumerable-property-symbols/-/get-own-enumerable-property-symbols-2.0.1.tgz#5c4ad87f2834c4b9b4e84549dc1e0650fb38c24b" + get-stdin@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-4.0.1.tgz#b968c6b0a04384324902e8bf1a5df32579a450fe" @@ -3886,6 +3942,14 @@ humanize-ms@^1.2.1: dependencies: ms "^2.0.0" +husky@^0.14.3: + version "0.14.3" + resolved "https://registry.yarnpkg.com/husky/-/husky-0.14.3.tgz#c69ed74e2d2779769a17ba8399b54ce0b63c12c3" + dependencies: + is-ci "^1.0.10" + normalize-path "^1.0.0" + strip-indent "^2.0.0" + i@0.3.x: version "0.3.5" resolved "https://registry.yarnpkg.com/i/-/i-0.3.5.tgz#1d2b854158ec8169113c6cb7f6b6801e99e211d5" @@ -3947,6 +4011,10 @@ indent-string@^2.1.0: dependencies: repeating "^2.0.0" +indent-string@^3.0.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-3.2.0.tgz#4a5fd6d27cc332f37e5419a504dbb837105c9289" + indexes-of@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/indexes-of/-/indexes-of-1.0.1.tgz#f30f716c8e2bd346c7b67d3df3915566a7c05607" @@ -4069,6 +4137,12 @@ is-callable@^1.1.1, is-callable@^1.1.3: version "1.1.3" resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.1.3.tgz#86eb75392805ddc33af71c92a0eedf74ee7604b2" +is-ci@^1.0.10: + version "1.0.10" + resolved "https://registry.yarnpkg.com/is-ci/-/is-ci-1.0.10.tgz#f739336b2632365061a9d48270cd56ae3369318e" + dependencies: + ci-info "^1.0.0" + is-data-descriptor@^0.1.4: version "0.1.4" resolved "https://registry.yarnpkg.com/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz#0b5ee648388e2c860282e793f1856fec3f301b56" @@ -4117,6 +4191,10 @@ is-extglob@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-1.0.0.tgz#ac468177c4943405a092fc8f29760c6ffc6206c0" +is-extglob@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" + is-finite@^1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/is-finite/-/is-finite-1.0.2.tgz#cc6677695602be550ef11e8b4aa6305342b6d0aa" @@ -4139,6 +4217,12 @@ is-glob@^2.0.0, is-glob@^2.0.1: dependencies: is-extglob "^1.0.0" +is-glob@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.0.tgz#9521c76845cc2610a85203ddf080a958c2ffabc0" + dependencies: + is-extglob "^2.1.1" + is-lower-case@^1.1.0: version "1.1.3" resolved "https://registry.yarnpkg.com/is-lower-case/-/is-lower-case-1.1.3.tgz#7e147be4768dc466db3bfb21cc60b31e6ad69393" @@ -4174,7 +4258,7 @@ is-number@^3.0.0: dependencies: kind-of "^3.0.2" -is-obj@^1.0.0: +is-obj@^1.0.0, is-obj@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/is-obj/-/is-obj-1.0.1.tgz#3e4729ac1f5fde025cd7d83a896dab9f4f67db0f" @@ -4218,6 +4302,10 @@ is-primitive@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/is-primitive/-/is-primitive-2.0.0.tgz#207bab91638499c07b2adf240a41a87210034575" +is-promise@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-promise/-/is-promise-2.1.0.tgz#79a2a9ece7f096e80f36d2b2f3bc16c1ff4bf3fa" + is-property@^1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/is-property/-/is-property-1.0.2.tgz#57fe1c4e48474edd65b09911f26b1cd4095dda84" @@ -4232,6 +4320,10 @@ is-regex@^1.0.4: dependencies: has "^1.0.1" +is-regexp@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-regexp/-/is-regexp-1.0.0.tgz#fd2d883545c46bac5a633e7b9a09e87fa2cb5069" + is-resolvable@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/is-resolvable/-/is-resolvable-1.0.0.tgz#8df57c61ea2e3c501408d100fb013cf8d6e0cc62" @@ -4330,6 +4422,19 @@ istanbul@^0.4.0: which "^1.1.1" wordwrap "^1.0.0" +jest-get-type@^21.2.0: + version "21.2.0" + resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-21.2.0.tgz#f6376ab9db4b60d81e39f30749c6c466f40d4a23" + +jest-validate@^21.1.0: + version "21.2.1" + resolved "https://registry.yarnpkg.com/jest-validate/-/jest-validate-21.2.1.tgz#cc0cbca653cd54937ba4f2a111796774530dd3c7" + dependencies: + chalk "^2.0.1" + jest-get-type "^21.2.0" + leven "^2.1.0" + pretty-format "^21.2.1" + jquery@^3.2.1: version "3.2.1" resolved "https://registry.yarnpkg.com/jquery/-/jquery-3.2.1.tgz#5c4d9de652af6cd0a770154a631bba12b015c787" @@ -4682,6 +4787,10 @@ lcid@^1.0.0: dependencies: invert-kv "^1.0.0" +leven@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/leven/-/leven-2.1.0.tgz#c2e7a9f772094dee9d34202ae8acce4687875580" + levn@^0.3.0, levn@~0.3.0: version "0.3.0" resolved "https://registry.yarnpkg.com/levn/-/levn-0.3.0.tgz#3b09924edf9f083c0490fdd4c0bc4421e04764ee" @@ -4702,6 +4811,72 @@ libnpx@~9.6.0: y18n "^3.2.1" yargs "^8.0.2" +lint-staged@^4.2.3: + version "4.2.3" + resolved "https://registry.yarnpkg.com/lint-staged/-/lint-staged-4.2.3.tgz#5a1f12256af06110b96225f109dbf215009a37a9" + dependencies: + app-root-path "^2.0.0" + chalk "^2.1.0" + cosmiconfig "^1.1.0" + execa "^0.8.0" + is-glob "^4.0.0" + jest-validate "^21.1.0" + listr "^0.12.0" + lodash "^4.17.4" + log-symbols "^2.0.0" + minimatch "^3.0.0" + npm-which "^3.0.1" + p-map "^1.1.1" + staged-git-files "0.0.4" + stringify-object "^3.2.0" + +listr-silent-renderer@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/listr-silent-renderer/-/listr-silent-renderer-1.1.1.tgz#924b5a3757153770bf1a8e3fbf74b8bbf3f9242e" + +listr-update-renderer@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/listr-update-renderer/-/listr-update-renderer-0.2.0.tgz#ca80e1779b4e70266807e8eed1ad6abe398550f9" + dependencies: + chalk "^1.1.3" + cli-truncate "^0.2.1" + elegant-spinner "^1.0.1" + figures "^1.7.0" + indent-string "^3.0.0" + log-symbols "^1.0.2" + log-update "^1.0.2" + strip-ansi "^3.0.1" + +listr-verbose-renderer@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/listr-verbose-renderer/-/listr-verbose-renderer-0.4.0.tgz#44dc01bb0c34a03c572154d4d08cde9b1dc5620f" + dependencies: + chalk "^1.1.3" + cli-cursor "^1.0.2" + date-fns "^1.27.2" + figures "^1.7.0" + +listr@^0.12.0: + version "0.12.0" + resolved "https://registry.yarnpkg.com/listr/-/listr-0.12.0.tgz#6bce2c0f5603fa49580ea17cd6a00cc0e5fa451a" + dependencies: + chalk "^1.1.3" + cli-truncate "^0.2.1" + figures "^1.7.0" + indent-string "^2.1.0" + is-promise "^2.1.0" + is-stream "^1.1.0" + listr-silent-renderer "^1.1.1" + listr-update-renderer "^0.2.0" + listr-verbose-renderer "^0.4.0" + log-symbols "^1.0.2" + log-update "^1.0.2" + ora "^0.2.3" + p-map "^1.1.1" + rxjs "^5.0.0-beta.11" + stream-to-observable "^0.1.0" + strip-ansi "^3.0.1" + load-grunt-tasks@3.5.2: version "3.5.2" resolved "https://registry.yarnpkg.com/load-grunt-tasks/-/load-grunt-tasks-3.5.2.tgz#0728561180fd20ff8a6927505852fc58aaea0c88" @@ -4892,7 +5067,7 @@ 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.0, log-symbols@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-1.0.2.tgz#376ff7b58ea3086a0f09facc74617eca501e1a18" dependencies: @@ -4904,6 +5079,13 @@ log-symbols@^2.0.0: dependencies: chalk "^2.0.1" +log-update@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/log-update/-/log-update-1.0.2.tgz#19929f64c4093d2d2e7075a1dad8af59c296b8d1" + dependencies: + 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" @@ -5518,6 +5700,10 @@ normalize-path@2.0.1, normalize-path@^2.0.0, normalize-path@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-2.0.1.tgz#47886ac1662760d4261b7d979d241709d3ce3f7a" +normalize-path@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-1.0.0.tgz#32d0e472f91ff345701c15a8311018d3b0a90379" + normalize-range@^0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/normalize-range/-/normalize-range-0.1.2.tgz#2d10c06bdfd312ea9777695a4d28439456b75942" @@ -5571,6 +5757,12 @@ npm-packlist@^1.1.6, npm-packlist@~1.1.8: ignore-walk "^3.0.0" npm-bundled "^1.0.1" +npm-path@^2.0.2: + version "2.0.3" + resolved "https://registry.yarnpkg.com/npm-path/-/npm-path-2.0.3.tgz#15cff4e1c89a38da77f56f6055b24f975dfb2bbe" + dependencies: + which "^1.2.10" + npm-pick-manifest@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/npm-pick-manifest/-/npm-pick-manifest-1.0.4.tgz#a5ee6510c1fe7221c0bc0414e70924c14045f7e8" @@ -5605,6 +5797,14 @@ npm-user-validate@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/npm-user-validate/-/npm-user-validate-1.0.0.tgz#8ceca0f5cea04d4e93519ef72d0557a75122e951" +npm-which@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/npm-which/-/npm-which-3.0.1.tgz#9225f26ec3a285c209cae67c3b11a6b4ab7140aa" + dependencies: + commander "^2.9.0" + npm-path "^2.0.2" + which "^1.2.10" + npm@^5.4.2: version "5.4.2" resolved "https://registry.yarnpkg.com/npm/-/npm-5.4.2.tgz#830b5cabb5f735264e7cc39b2163b90854b2eaa8" @@ -5844,6 +6044,15 @@ 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" + dependencies: + chalk "^1.1.1" + cli-cursor "^1.0.2" + cli-spinners "^0.1.2" + object-assign "^4.0.1" + ordered-ast-traverse@~1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/ordered-ast-traverse/-/ordered-ast-traverse-1.1.1.tgz#6843a170bc0eee8b520cc8ddc1ddd3aa30fa057c" @@ -5901,6 +6110,10 @@ p-locate@^2.0.0: dependencies: p-limit "^1.1.0" +p-map@^1.1.1: + version "1.2.0" + resolved "https://registry.yarnpkg.com/p-map/-/p-map-1.2.0.tgz#e4e94f311eabbc8633a1e79908165fca26241b6b" + package-json@^4.0.0: version "4.0.1" resolved "https://registry.yarnpkg.com/package-json/-/package-json-4.0.1.tgz#8869a0401253661c4c4ca3da6c2121ed555f5eed" @@ -6473,6 +6686,10 @@ preserve@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/preserve/-/preserve-0.2.0.tgz#815ed1f6ebc65926f865b310c0713bcb3315ce4b" +prettier@1.7.3: + version "1.7.3" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.7.3.tgz#8e6974725273914b1c47439959dd3d3ba53664b6" + pretty-bytes@^1.0.0: version "1.0.4" resolved "https://registry.yarnpkg.com/pretty-bytes/-/pretty-bytes-1.0.4.tgz#0a22e8210609ad35542f8c8d5d2159aff0751c84" @@ -6491,6 +6708,13 @@ pretty-error@^2.0.2: renderkid "^2.0.1" utila "~0.4" +pretty-format@^21.2.1: + version "21.2.1" + resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-21.2.1.tgz#ae5407f3cf21066cd011aa1ba5fce7b6a2eddb36" + dependencies: + ansi-regex "^3.0.0" + ansi-styles "^3.2.0" + private@^0.1.6, private@^0.1.7, private@~0.1.5: version "0.1.7" resolved "https://registry.yarnpkg.com/private/-/private-0.1.7.tgz#68ce5e8a1ef0a23bb570cc28537b5332aba63ef1" @@ -7143,7 +7367,7 @@ rx-lite@^3.1.2: version "3.1.2" resolved "https://registry.yarnpkg.com/rx-lite/-/rx-lite-3.1.2.tgz#19ce502ca572665f3b647b10939f97fd1615f102" -rxjs@^5.4.3: +rxjs@^5.0.0-beta.11, rxjs@^5.4.3: version "5.4.3" resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-5.4.3.tgz#0758cddee6033d68e0fd53676f0f3596ce3d483f" dependencies: @@ -7623,6 +7847,10 @@ stack-trace@0.0.x: version "0.0.10" resolved "https://registry.yarnpkg.com/stack-trace/-/stack-trace-0.0.10.tgz#547c70b347e8d32b4e108ea1a2a159e5fdde19c0" +staged-git-files@0.0.4: + version "0.0.4" + resolved "https://registry.yarnpkg.com/staged-git-files/-/staged-git-files-0.0.4.tgz#d797e1b551ca7a639dec0237dc6eb4bb9be17d35" + static-extend@^0.1.1: version "0.1.2" resolved "https://registry.yarnpkg.com/static-extend/-/static-extend-0.1.2.tgz#60809c39cbff55337226fd5e0b520f341f1fb5c6" @@ -7679,6 +7907,10 @@ stream-shift@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/stream-shift/-/stream-shift-1.0.0.tgz#d5c752825e5367e786f78e18e445ea223a155952" +stream-to-observable@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/stream-to-observable/-/stream-to-observable-0.1.0.tgz#45bf1d9f2d7dc09bed81f1c307c430e68b84cffe" + strict-uri-encode@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz#279b225df1d582b1f54e65addd4352e18faa0713" @@ -7714,6 +7946,14 @@ string_decoder@~1.0.3: dependencies: safe-buffer "~5.1.0" +stringify-object@^3.2.0: + version "3.2.1" + resolved "https://registry.yarnpkg.com/stringify-object/-/stringify-object-3.2.1.tgz#2720c2eff940854c819f6ee252aaeb581f30624d" + dependencies: + get-own-enumerable-property-symbols "^2.0.1" + is-obj "^1.0.1" + is-regexp "^1.0.0" + stringmap@~0.2.2: version "0.2.2" resolved "https://registry.yarnpkg.com/stringmap/-/stringmap-0.2.2.tgz#556c137b258f942b8776f5b2ef582aa069d7d1b1" @@ -7758,6 +7998,10 @@ strip-indent@^1.0.1: dependencies: get-stdin "^4.0.1" +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: version "1.0.4" resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-1.0.4.tgz#1e15fbcac97d3ee99bf2d73b4c656b082bbafb91" @@ -8502,7 +8746,7 @@ which-module@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a" -which@1, which@^1.1.1, which@^1.2.1, which@^1.2.14, which@^1.2.4, which@^1.2.9, which@^1.3.0, which@~1.3.0: +which@1, which@^1.1.1, which@^1.2.1, which@^1.2.10, which@^1.2.14, which@^1.2.4, which@^1.2.9, which@^1.3.0, which@~1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/which/-/which-1.3.0.tgz#ff04bdfc010ee547d780bec38e1ac1c2777d253a" dependencies: From a365719d3f1246177726dfc1e79efdd24904f033 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 2 Oct 2017 17:25:43 +0200 Subject: [PATCH 23/83] tech: minor npm scripts update --- package.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 1bbe44991b9..cd8659842f2 100644 --- a/package.json +++ b/package.json @@ -96,9 +96,9 @@ "scripts": { "dev": "./node_modules/.bin/webpack --progress --colors --config scripts/webpack/webpack.dev.js", "watch": "./node_modules/.bin/webpack --progress --colors --watch --config scripts/webpack/webpack.dev.js", - "build": "./node_modules/grunt-cli/bin/grunt build", - "test": "./node_modules/grunt-cli/bin/grunt test", - "lint" : "node ./node_modules/tslint/lib/tslint-cli.js -c tslint.json --project ./tsconfig.json --type-check", + "build": "./node_modules/.bin/grunt build", + "test": "./node_modules/.bin/grunt test", + "lint" : "./node_modules/.bin/tslint -c tslint.json --project ./tsconfig.json --type-check", "watch-test": "./node_modules/grunt-cli/bin/grunt karma:dev" }, "license": "Apache-2.0", From 122e2b5c426d39b508f5a04fc05223e680cdb531 Mon Sep 17 00:00:00 2001 From: Matthew McGinn Date: Mon, 2 Oct 2017 20:45:22 -0400 Subject: [PATCH 24/83] break out slack upload into separate function --- pkg/services/alerting/notifiers/slack.go | 38 ++++++++++++++---------- 1 file changed, 22 insertions(+), 16 deletions(-) diff --git a/pkg/services/alerting/notifiers/slack.go b/pkg/services/alerting/notifiers/slack.go index 1ee16453a5f..908aab612ec 100644 --- a/pkg/services/alerting/notifiers/slack.go +++ b/pkg/services/alerting/notifiers/slack.go @@ -163,29 +163,35 @@ func (this *SlackNotifier) Notify(evalContext *alerting.EvalContext) error { return err } if this.Token != "" { - slackUploadUrl := "https://slack.com/api/files.upload" - if evalContext.ImageOnDiskPath == "" { - evalContext.ImageOnDiskPath = "public/img/mixed_styles.png" - } - this.log.Info("Uploading to slack via file.upload API") - headers, uploadBody, err := GenerateSlackUpload(evalContext.ImageOnDiskPath, this.Token, this.Recipient) - if err != nil { - return err - } - cmd := &m.SendWebhookSync{Url: slackUploadUrl, Body: uploadBody.String(), HttpHeader: headers, HttpMethod: "POST"} - if err := bus.DispatchCtx(evalContext.Ctx, cmd); err != nil { - this.log.Error("Failed to upload slack image", "error", err, "webhook", "file.upload") - return err - } + err = SlackFileUpload(evalContext, this.log, "https://slack.com/api/files.upload", this.Recipient, this.Token) if err != nil { return err } } - return nil } -func GenerateSlackUpload(file string, token string, recipient string) (map[string]string, bytes.Buffer, error) { +func SlackFileUpload(evalContext *alerting.EvalContext, log log.Logger, url string, recipient string, token string) error { + if evalContext.ImageOnDiskPath == "" { + evalContext.ImageOnDiskPath = "public/img/mixed_styles.png" + } + log.Info("Uploading to slack via file.upload API") + headers, uploadBody, err := GenerateSlackBody(evalContext.ImageOnDiskPath, token, recipient) + if err != nil { + return err + } + cmd := &m.SendWebhookSync{Url: url, Body: uploadBody.String(), HttpHeader: headers, HttpMethod: "POST"} + if err := bus.DispatchCtx(evalContext.Ctx, cmd); err != nil { + log.Error("Failed to upload slack image", "error", err, "webhook", "file.upload") + return err + } + if err != nil { + return err + } + return nil +} + +func GenerateSlackBody(file string, token string, recipient string) (map[string]string, bytes.Buffer, error) { // Slack requires all POSTs to files.upload to present // an "application/x-www-form-urlencoded" encoded querystring // See https://api.slack.com/methods/files.upload From be0d47146751733e60807240c5082ea777a74109 Mon Sep 17 00:00:00 2001 From: Matthew McGinn Date: Mon, 2 Oct 2017 23:18:48 -0400 Subject: [PATCH 25/83] properly parse & pass upload image bool from config --- pkg/services/alerting/notifiers/slack.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pkg/services/alerting/notifiers/slack.go b/pkg/services/alerting/notifiers/slack.go index 908aab612ec..ed1451da419 100644 --- a/pkg/services/alerting/notifiers/slack.go +++ b/pkg/services/alerting/notifiers/slack.go @@ -74,6 +74,7 @@ func NewSlackNotifier(model *m.AlertNotification) (alerting.Notifier, error) { recipient := model.Settings.Get("recipient").MustString() mention := model.Settings.Get("mention").MustString() token := model.Settings.Get("token").MustString() + uploadImage := model.Settings.Get("uploadImage").MustBool(true) return &SlackNotifier{ NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.Settings), @@ -81,6 +82,7 @@ func NewSlackNotifier(model *m.AlertNotification) (alerting.Notifier, error) { Recipient: recipient, Mention: mention, Token: token, + Upload: uploadImage, log: log.New("alerting.notifier.slack"), }, nil } @@ -91,6 +93,7 @@ type SlackNotifier struct { Recipient string Mention string Token string + Upload bool log log.Logger } @@ -162,7 +165,7 @@ func (this *SlackNotifier) Notify(evalContext *alerting.EvalContext) error { this.log.Error("Failed to send slack notification", "error", err, "webhook", this.Name) return err } - if this.Token != "" { + if this.Token != "" && this.UploadImage { err = SlackFileUpload(evalContext, this.log, "https://slack.com/api/files.upload", this.Recipient, this.Token) if err != nil { return err From 4c4564b1380bb3d89bbf0a489b9562645ed810d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Tue, 3 Oct 2017 11:02:40 +0200 Subject: [PATCH 26/83] plugibs: expose more to plugins --- package.json | 2 +- public/app/core/live/live_srv.ts | 2 - .../app/features/plugins/buit_in_plugins.ts | 47 ++++++++++++++++ public/app/features/plugins/plugin_loader.ts | 56 +++---------------- scripts/webpack/dependencies.js | 14 +++++ scripts/webpack/webpack.dev.js | 18 ++---- scripts/webpack/webpack.prod.js | 4 +- 7 files changed, 76 insertions(+), 67 deletions(-) create mode 100644 public/app/features/plugins/buit_in_plugins.ts create mode 100644 scripts/webpack/dependencies.js diff --git a/package.json b/package.json index cd8659842f2..83025a3e8fc 100644 --- a/package.json +++ b/package.json @@ -77,7 +77,6 @@ "postcss-reporter": "^5.0.0", "prettier": "1.7.3", "react-test-renderer": "^16.0.0", - "rxjs": "^5.4.3", "sass-lint": "^1.10.2", "sass-loader": "^6.0.6", "sinon": "1.17.6", @@ -119,6 +118,7 @@ "mousetrap": "^1.6.0", "ngreact": "^0.4.1", "react": "^16.0.0", + "rxjs": "^5.4.3", "react-dom": "^16.0.0", "remarkable": "^1.7.1", "tether": "^1.4.0", diff --git a/public/app/core/live/live_srv.ts b/public/app/core/live/live_srv.ts index 8368992d052..8e808e60e1a 100644 --- a/public/app/core/live/live_srv.ts +++ b/public/app/core/live/live_srv.ts @@ -1,5 +1,3 @@ -/// - import _ from 'lodash'; import config from 'app/core/config'; diff --git a/public/app/features/plugins/buit_in_plugins.ts b/public/app/features/plugins/buit_in_plugins.ts new file mode 100644 index 00000000000..7ca32696388 --- /dev/null +++ b/public/app/features/plugins/buit_in_plugins.ts @@ -0,0 +1,47 @@ +import * as graphitePlugin from 'app/plugins/datasource/graphite/module'; +import * as cloudwatchPlugin from 'app/plugins/datasource/cloudwatch/module'; +import * as elasticsearchPlugin from 'app/plugins/datasource/elasticsearch/module'; +import * as opentsdbPlugin from 'app/plugins/datasource/opentsdb/module'; +import * as grafanaPlugin from 'app/plugins/datasource/grafana/module'; +import * as influxdbPlugin from 'app/plugins/datasource/influxdb/module'; +import * as mixedPlugin from 'app/plugins/datasource/mixed/module'; +import * as mysqlPlugin from 'app/plugins/datasource/mysql/module'; +import * as prometheusPlugin from 'app/plugins/datasource/prometheus/module'; + +import * as textPanel from 'app/plugins/panel/text/module'; +import * as graphPanel from 'app/plugins/panel/graph/module'; +import * as dashListPanel from 'app/plugins/panel/dashlist/module'; +import * as pluginsListPanel from 'app/plugins/panel/pluginlist/module'; +import * as alertListPanel from 'app/plugins/panel/alertlist/module'; +import * as heatmapPanel from 'app/plugins/panel/heatmap/module'; +import * as tablePanel from 'app/plugins/panel/table/module'; +import * as singlestatPanel from 'app/plugins/panel/singlestat/module'; +import * as gettingStartedPanel from 'app/plugins/panel/gettingstarted/module'; +import * as testDataAppPlugin from 'app/plugins/app/testdata/module'; +import * as testDataDSPlugin from 'app/plugins/app/testdata/datasource/module'; + +const builtInPlugins = { + "app/plugins/datasource/graphite/module": graphitePlugin, + "app/plugins/datasource/cloudwatch/module": cloudwatchPlugin, + "app/plugins/datasource/elasticsearch/module": elasticsearchPlugin, + "app/plugins/datasource/opentsdb/module": opentsdbPlugin, + "app/plugins/datasource/grafana/module": grafanaPlugin, + "app/plugins/datasource/influxdb/module": influxdbPlugin, + "app/plugins/datasource/mixed/module": mixedPlugin, + "app/plugins/datasource/mysql/module": mysqlPlugin, + "app/plugins/datasource/prometheus/module": prometheusPlugin, + "app/plugins/app/testdata/module": testDataAppPlugin, + "app/plugins/app/testdata/datasource/module": testDataDSPlugin, + + "app/plugins/panel/text/module": textPanel, + "app/plugins/panel/graph/module": graphPanel, + "app/plugins/panel/dashlist/module": dashListPanel, + "app/plugins/panel/pluginlist/module": pluginsListPanel, + "app/plugins/panel/alertlist/module": alertListPanel, + "app/plugins/panel/heatmap/module": heatmapPanel, + "app/plugins/panel/table/module": tablePanel, + "app/plugins/panel/singlestat/module": singlestatPanel, + "app/plugins/panel/gettingstarted/module": gettingStartedPanel, +}; + +export default builtInPlugins; diff --git a/public/app/features/plugins/plugin_loader.ts b/public/app/features/plugins/plugin_loader.ts index ac6fc06ce28..8df27c0f320 100644 --- a/public/app/features/plugins/plugin_loader.ts +++ b/public/app/features/plugins/plugin_loader.ts @@ -7,53 +7,11 @@ import angular from 'angular'; import jquery from 'jquery'; import config from 'app/core/config'; import TimeSeries from 'app/core/time_series2'; +import TableModel from 'app/core/table_model'; +import {Observable} from 'rxjs/Observable'; +import {Subject} from 'rxjs/Subject'; import * as datemath from 'app/core/utils/datemath'; - -import * as graphitePlugin from 'app/plugins/datasource/graphite/module'; -import * as cloudwatchPlugin from 'app/plugins/datasource/cloudwatch/module'; -import * as elasticsearchPlugin from 'app/plugins/datasource/elasticsearch/module'; -import * as opentsdbPlugin from 'app/plugins/datasource/opentsdb/module'; -import * as grafanaPlugin from 'app/plugins/datasource/grafana/module'; -import * as influxdbPlugin from 'app/plugins/datasource/influxdb/module'; -import * as mixedPlugin from 'app/plugins/datasource/mixed/module'; -import * as mysqlPlugin from 'app/plugins/datasource/mysql/module'; -import * as prometheusPlugin from 'app/plugins/datasource/prometheus/module'; - -import * as textPanel from 'app/plugins/panel/text/module'; -import * as graphPanel from 'app/plugins/panel/graph/module'; -import * as dashListPanel from 'app/plugins/panel/dashlist/module'; -import * as pluginsListPanel from 'app/plugins/panel/pluginlist/module'; -import * as alertListPanel from 'app/plugins/panel/alertlist/module'; -import * as heatmapPanel from 'app/plugins/panel/heatmap/module'; -import * as tablePanel from 'app/plugins/panel/table/module'; -import * as singlestatPanel from 'app/plugins/panel/singlestat/module'; -import * as gettingStartedPanel from 'app/plugins/panel/gettingstarted/module'; -import * as testDataAppPlugin from 'app/plugins/app/testdata/module'; -import * as testDataDSPlugin from 'app/plugins/app/testdata/datasource/module'; - -let builtInPlugins = { - "app/plugins/datasource/graphite/module": graphitePlugin, - "app/plugins/datasource/cloudwatch/module": cloudwatchPlugin, - "app/plugins/datasource/elasticsearch/module": elasticsearchPlugin, - "app/plugins/datasource/opentsdb/module": opentsdbPlugin, - "app/plugins/datasource/grafana/module": grafanaPlugin, - "app/plugins/datasource/influxdb/module": influxdbPlugin, - "app/plugins/datasource/mixed/module": mixedPlugin, - "app/plugins/datasource/mysql/module": mysqlPlugin, - "app/plugins/datasource/prometheus/module": prometheusPlugin, - "app/plugins/app/testdata/module": testDataAppPlugin, - "app/plugins/app/testdata/datasource/module": testDataDSPlugin, - - "app/plugins/panel/text/module": textPanel, - "app/plugins/panel/graph/module": graphPanel, - "app/plugins/panel/dashlist/module": dashListPanel, - "app/plugins/panel/pluginlist/module": pluginsListPanel, - "app/plugins/panel/alertlist/module": alertListPanel, - "app/plugins/panel/heatmap/module": heatmapPanel, - "app/plugins/panel/table/module": tablePanel, - "app/plugins/panel/singlestat/module": singlestatPanel, - "app/plugins/panel/gettingstarted/module": gettingStartedPanel, -}; +import builtInPlugins from './buit_in_plugins'; System.config({ baseURL: 'public', @@ -89,12 +47,16 @@ exposeToPlugin('lodash', _); exposeToPlugin('moment', moment); exposeToPlugin('jquery', jquery); exposeToPlugin('angular', angular); +exposeToPlugin('rxjs/Subject', Subject); +exposeToPlugin('rxjs/Observable', Observable); + exposeToPlugin('app/plugins/sdk', sdk); exposeToPlugin('app/core/utils/datemath', datemath); exposeToPlugin('app/core/utils/kbn', kbn); exposeToPlugin('app/core/config', config); exposeToPlugin('app/core/time_series', TimeSeries); exposeToPlugin('app/core/time_series2', TimeSeries); +exposeToPlugin('app/core/table_model', TableModel); import 'vendor/flot/jquery.flot'; import 'vendor/flot/jquery.flot.selection'; @@ -107,7 +69,7 @@ import 'vendor/flot/jquery.flot.crosshair'; import 'vendor/flot/jquery.flot.dashes'; for (let flotDep of ['jquery.flot', 'jquery.flot.pie', 'jquery.flot.time']) { - System.registerDynamic(flotDep, [], true, function(require, exports, module) { module.exports = {fakeDep: 1}; }); + exposeToPlugin(flotDep, {fakeDep: 1}); } export function importPluginModule(path: string): Promise { diff --git a/scripts/webpack/dependencies.js b/scripts/webpack/dependencies.js new file mode 100644 index 00000000000..3bdec887ae9 --- /dev/null +++ b/scripts/webpack/dependencies.js @@ -0,0 +1,14 @@ +'use strict'; + +const pkg = require('../../package.json'); +const _ = require('lodash'); + +let dependencies = Object.keys(pkg.dependencies); +// remove jquery so we can add it first +// remove rxjs so we can only depend on parts of it in code +_.pull(dependencies, 'jquery', 'rxjs') + +// add jquery first +dependencies.unshift('jquery'); + +module.exports = dependencies; diff --git a/scripts/webpack/webpack.dev.js b/scripts/webpack/webpack.dev.js index 98ecb44e284..51b9a646175 100644 --- a/scripts/webpack/webpack.dev.js +++ b/scripts/webpack/webpack.dev.js @@ -8,16 +8,6 @@ const HtmlWebpackPlugin = require("html-webpack-plugin"); const ExtractTextPlugin = require("extract-text-webpack-plugin"); const WebpackCleanupPlugin = require('webpack-cleanup-plugin'); const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin; -const pkg = require('../../package.json'); -const _ = require('lodash'); - -let dependencies = Object.keys(pkg.dependencies); -// remove jquery -dependencies = _.filter(dependencies, function(key) { - return key !== 'jquery'; -}); -// add it first -dependencies.unshift('jquery'); module.exports = merge(common, { devtool: "source-map", @@ -25,7 +15,7 @@ module.exports = merge(common, { entry: { dark: './public/sass/grafana.dark.scss', light: './public/sass/grafana.light.scss', - vendor: dependencies, + vendor: require('./dependencies'), }, module: { @@ -55,8 +45,8 @@ module.exports = merge(common, { names: ['vendor', 'manifest'], }), new WebpackCleanupPlugin(), - // new BundleAnalyzerPlugin({ - // analyzerPort: 8889 - // }) + new BundleAnalyzerPlugin({ + analyzerPort: 8889 + }) ] }); diff --git a/scripts/webpack/webpack.prod.js b/scripts/webpack/webpack.prod.js index c43b2056e47..6d254c7ab0b 100644 --- a/scripts/webpack/webpack.prod.js +++ b/scripts/webpack/webpack.prod.js @@ -8,8 +8,6 @@ const path = require('path'); const ngAnnotatePlugin = require('ng-annotate-webpack-plugin'); const HtmlWebpackPlugin = require("html-webpack-plugin"); const ExtractTextPlugin = require("extract-text-webpack-plugin"); -const pkg = require('../../package.json'); -let dependencies = Object.keys(pkg.dependencies); module.exports = merge(common, { devtool: "source-map", @@ -17,7 +15,7 @@ module.exports = merge(common, { entry: { dark: './public/sass/grafana.dark.scss', light: './public/sass/grafana.light.scss', - vendor: dependencies, + vendor: require('./dependencies'), }, module: { From 3dac77468bcc5c814a9a93a2cf603aea9126da2a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Tue, 3 Oct 2017 12:18:22 +0200 Subject: [PATCH 27/83] fix: ensure panel.datasource is null as default --- public/app/features/panel/metrics_panel_ctrl.ts | 1 + scripts/webpack/webpack.dev.js | 6 +++--- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/public/app/features/panel/metrics_panel_ctrl.ts b/public/app/features/panel/metrics_panel_ctrl.ts index 38d9c631b54..839dbba72e8 100644 --- a/public/app/features/panel/metrics_panel_ctrl.ts +++ b/public/app/features/panel/metrics_panel_ctrl.ts @@ -41,6 +41,7 @@ class MetricsPanelCtrl extends PanelCtrl { this.timeSrv = $injector.get('timeSrv'); this.templateSrv = $injector.get('templateSrv'); this.scope = $scope; + this.panel.datasource = this.panel.datasource || null; if (!this.panel.targets) { this.panel.targets = [{}]; diff --git a/scripts/webpack/webpack.dev.js b/scripts/webpack/webpack.dev.js index 51b9a646175..ab48864b8a0 100644 --- a/scripts/webpack/webpack.dev.js +++ b/scripts/webpack/webpack.dev.js @@ -45,8 +45,8 @@ module.exports = merge(common, { names: ['vendor', 'manifest'], }), new WebpackCleanupPlugin(), - new BundleAnalyzerPlugin({ - analyzerPort: 8889 - }) + // new BundleAnalyzerPlugin({ + // analyzerPort: 8889 + // }) ] }); From 50e2f7e366fd7f9b025566af538e1689cb7c3120 Mon Sep 17 00:00:00 2001 From: Patrick O'Carroll Date: Tue, 3 Oct 2017 13:23:05 +0200 Subject: [PATCH 28/83] Checkbox fixes (#9400) * fixing checkboxes * deleted old file, fixed checkboxes * minor change --- .../features/admin/partials/edit_user.html | 2 +- public/app/features/org/partials/invite.html | 8 ++-- public/app/partials/roweditor.html | 47 ------------------- .../partials/annotations.editor.html | 22 ++++----- .../opentsdb/partials/annotations.editor.html | 5 +- 5 files changed, 15 insertions(+), 69 deletions(-) delete mode 100644 public/app/partials/roweditor.html diff --git a/public/app/features/admin/partials/edit_user.html b/public/app/features/admin/partials/edit_user.html index b907f794e76..384601affb7 100644 --- a/public/app/features/admin/partials/edit_user.html +++ b/public/app/features/admin/partials/edit_user.html @@ -41,7 +41,7 @@
    - +
    diff --git a/public/app/features/org/partials/invite.html b/public/app/features/org/partials/invite.html index 3bdf51d15a5..e1be0b5648e 100644 --- a/public/app/features/org/partials/invite.html +++ b/public/app/features/org/partials/invite.html @@ -41,15 +41,13 @@
    -
    diff --git a/public/app/partials/roweditor.html b/public/app/partials/roweditor.html deleted file mode 100644 index d414cb151f3..00000000000 --- a/public/app/partials/roweditor.html +++ /dev/null @@ -1,47 +0,0 @@ -
    -

    - Row settings -

    - - -
    - -
    -
    -
    -
    -
    Row details
    -
    -
    -
    - -
    - Title - -
    -
    - Height - - -
    -
    -
    -
    -
    -
    -
    Templating options
    -
    -
    -
    - Repeat Row -
    - -
    - -
    - - +
    + +
    + Action + +
    +
    + Alarm Name + +
    diff --git a/public/app/plugins/datasource/opentsdb/partials/annotations.editor.html b/public/app/plugins/datasource/opentsdb/partials/annotations.editor.html index 6c5cc5450bf..914672053ae 100644 --- a/public/app/plugins/datasource/opentsdb/partials/annotations.editor.html +++ b/public/app/plugins/datasource/opentsdb/partials/annotations.editor.html @@ -3,8 +3,5 @@ OpenTSDB metrics query
    -
    - Show Global Annotations? - -
    +
    From dd83b67de589f0a006dfc46b96e00651fdf04ed4 Mon Sep 17 00:00:00 2001 From: Patrick O'Carroll Date: Tue, 3 Oct 2017 13:34:52 +0200 Subject: [PATCH 29/83] Docs text fixes (#9408) * styling changes and some text changes * styling changes --- docs/sources/features/panels/table_panel.md | 28 ++++++++++----------- docs/sources/features/panels/text.md | 4 +-- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/docs/sources/features/panels/table_panel.md b/docs/sources/features/panels/table_panel.md index 9c112c81265..a3e56c72960 100644 --- a/docs/sources/features/panels/table_panel.md +++ b/docs/sources/features/panels/table_panel.md @@ -25,8 +25,8 @@ The table panel has many ways to manipulate your data for optimal presentation. {{< docs-imagebox img="/img/docs/v45/table_options.png" class="docs-image--no-shadow" max-width= "500px" >}} -1. `Data`: Control how your query is transformed into a table. -2. `Paging`: Table display options. +1. **Data**: Control how your query is transformed into a table. +2. **Paging**: Table display options. ## Data to Table @@ -43,20 +43,20 @@ you want in the table. Only applicable for some transforms. {{< docs-imagebox img="/img/docs/v45/table_ts_to_rows.png" >}} -In the most simple mode you can turn time series to rows. This means you get a `Time`, `Metric` and a `Value` column. Where `Metric` is the name of the time series. +In the most simple mode you can turn time series to rows. This means you get a **Time**, **Metric** and a **Value** column. Where **Metric** is the name of the time series. ### Time series to columns {{< docs-imagebox img="/img/docs/v45/table_ts_to_columns.png" >}} -This transform allows you to take multiple time series and group them by time. Which will result in the primary column being `Time` and a column for each time series. +This transform allows you to take multiple time series and group them by time. Which will result in the primary column being **Time** and a column for each time series. ### Time series aggregations {{< docs-imagebox img="/img/docs/v45/table_ts_to_aggregations.png" >}} -This table transformation will lay out your table into rows by metric, allowing columns of `Avg`, `Min`, `Max`, `Total`, `Current` and `Count`. More than one column can be added. +This table transformation will lay out your table into rows by metric, allowing columns of **Avg**, **Min**, **Max**, **Total**, **Current** and **Count**. More than one column can be added. ### Annotations @@ -70,7 +70,7 @@ mode then any queries you have in the metrics tab will be ignored. {{< docs-imagebox img="/img/docs/v45/table_json_data.png" max-width="500px" >}} -If you have an Elasticsearch **Raw Document** query or an Elasticsearch query without a `date histogram` use this +If you have an Elasticsearch **Raw Document** query or an Elasticsearch query without a **date histogram** use this transform mode and pick the columns using the **Columns** section. @@ -80,9 +80,9 @@ transform mode and pick the columns using the **Columns** section. {{< docs-imagebox img="/img/docs/v45/table_paging.png" class="docs-image--no-shadow docs-image--right" max-width="350px" >}} -1. `Pagination (Page Size)`: The table display fields allow you to control The `Pagination` (page size) is the threshold at which the table rows will be broken into pages. For example, if your table had 95 records with a pagination value of 10, your table would be split across 9 pages. -2. `Scroll`: The `scroll bar` checkbox toggles the ability to scroll within the panel, when unchecked, the panel height will grow to display all rows. -3. `Font Size`: The `font size` field allows you to increase or decrease the size for the panel, relative to the default font size. +1. **Rows Per Page**: The table display fields allow you to control how many rows per page there should be. For example, if your table had 95 records with a rows per page value of 10, your table would be split across 10 pages. +2. **Scroll**: The scroll bar checkbox toggles the ability to scroll within the panel, when unchecked, the panel height will grow to display all rows. +3. **Font Size**: The font size field allows you to increase or decrease the size for the panel, relative to the default font size. ## Column Styles @@ -91,9 +91,9 @@ The column styles allow you control how dates and numbers are formatted. {{< docs-imagebox img="/img/docs/v45/table_column_styles.png" class="docs-image--no-shadow" >}} -1. `Name or regex`: The Name or Regex field controls what columns the rule should be applied to. The regex or name filter will be matched against the column name not against column values. -2. `Column Header`: Title for the column, when using a Regex the title can include replacement strings like `$1`. -3. `Add column style rule`: Add new column rule. -4. `Thresholds` and `Coloring`: Specify color mode and thresholds limits. -5. `Type`: The three supported types of types are `Number`, `String` and `Date`. `Unit` and `Decimals`: Specify unit and decimal precision for numbers.`Format`: Specify date format for dates. +1. **Name or regex**: The Name or Regex field controls what columns the rule should be applied to. The regex or name filter will be matched against the column name not against column values. +2. **Column Header**: Title for the column, when using a Regex the title can include replacement strings like `$1`. +3. **Add column style rule**: Add new column rule. +4. **Thresholds and Coloring**: Specify color mode and thresholds limits. +5. **Type**: The three supported types of types are **Number**, **String** and **Date**. **Unit** and **Decimals**: Specify unit and decimal precision for numbers. **Format**: Specify date format for dates. diff --git a/docs/sources/features/panels/text.md b/docs/sources/features/panels/text.md index ae37df16b9e..cc8269793bd 100644 --- a/docs/sources/features/panels/text.md +++ b/docs/sources/features/panels/text.md @@ -18,6 +18,6 @@ The text panel lets you make information and description panels etc. for your da {{< docs-imagebox img="/img/docs/v45/text-options.png" max-width="600px" class="docs-image--no-shadow">}} -1. `Mode`: Here you can choose between markdown, HTML or text. -2. `Content`: Here you write your content. +1. **Mode**: Here you can choose between markdown, HTML or text. +2. **Content**: Here you write your content. From e1a86e63b87374732893948a62ae4d93c46b60e1 Mon Sep 17 00:00:00 2001 From: Patrick O'Carroll Date: Tue, 3 Oct 2017 13:35:12 +0200 Subject: [PATCH 30/83] some restyling (#9409) --- docs/sources/features/panels/graph.md | 68 ++++++++++------------ docs/sources/features/panels/singlestat.md | 54 ++++++++--------- 2 files changed, 59 insertions(+), 63 deletions(-) diff --git a/docs/sources/features/panels/graph.md b/docs/sources/features/panels/graph.md index 8933864d2d9..c3b0260c98b 100644 --- a/docs/sources/features/panels/graph.md +++ b/docs/sources/features/panels/graph.md @@ -56,44 +56,46 @@ options. {{< docs-imagebox img="/img/docs/v43/graph_axes_grid_options.png" max-width= "900px" >}} -The Axes tab controls the display of axes, grids and legend. The ``Left Y`` and ``Right Y`` can be customized using: +The Axes tab controls the display of axes, grids and legend. The **Left Y** and **Right Y** can be customized using: -- ``Unit`` - The display unit for the Y value -- ``Grid Max`` - The maximum Y value. (default auto) -- ``Grid Min`` - The minimum Y value. (default auto) -- ``Label`` - The Y axis label (default "") +- **Unit** - The display unit for the Y value +- **Scale** - +- **Y-Min** - The minimum Y value. (default auto) +- **Y-Max** - The maximum Y value. (default auto) +- **Label** - The Y axis label (default "") -Axes can also be hidden by unchecking the appropriate box from `Show Axis`. +Axes can also be hidden by unchecking the appropriate box from **Show**. ### X-Axis Mode There are three options: -- The default option is `Time` and means the x-axis represents time and that the data is grouped by time (for example, by hour or by minute). +- The default option is **Time** and means the x-axis represents time and that the data is grouped by time (for example, by hour or by minute). -- The `Series` option means that the data is grouped by series and not by time. The y-axis still represents the value. +- The **Series** option means that the data is grouped by series and not by time. The y-axis still represents the value. {{< docs-imagebox img="/img/docs/v45/graph-x-axis-mode-series.png" max-width="700px">}} -- The `Histogram` option converts the graph into a histogram. A Histogram is a kind of bar chart that groups numbers into ranges, often called buckets or bins. Taller bars show that more data falls in that range. Histograms and buckets are described in more detail [here](http://docs.grafana.org/features/panels/heatmap/#histograms-and-buckets). +- The **Histogram** option converts the graph into a histogram. A Histogram is a kind of bar chart that groups numbers into ranges, often called buckets or bins. Taller bars show that more data falls in that range. Histograms and buckets are described in more detail [here](http://docs.grafana.org/features/panels/heatmap/#histograms-and-buckets). ### Legend -The legend hand be hidden by checking the ``Show`` checkbox. If it's shown, it can be -displayed as a table of values by checking the ``Table`` checkbox. Series with no -values can be hidden from the legend using the ``Hide empty`` checkbox. +The legend hand be hidden by checking the **Show** checkbox. If it's shown, it can be +displayed as a table of values by checking the **Table** checkbox. Series with no +values can be hidden from the legend using the **Hide empty** checkbox. ### Legend Values Additional values can be shown along-side the legend names: -- ``Total`` - Sum of all values returned from metric query -- ``Current`` - Last value returned from the metric query -- ``Min`` - Minimum of all values returned from metric query -- ``Max`` - Maximum of all values returned from the metric query -- ``Avg`` - Average of all values returned from metric query -- ``Decimals`` - Controls how many decimals are displayed for legend values (and graph hover tooltips) + +- **Total** - Sum of all values returned from metric query +- **Current** - Last value returned from the metric query +- **Min** - Minimum of all values returned from metric query +- **Max** - Maximum of all values returned from the metric query +- **Avg** - Average of all values returned from metric query +- **Decimals** - Controls how many decimals are displayed for legend values (and graph hover tooltips) The legend values are calculated client side by Grafana and depend on what type of aggregation or point consolidation your metric query is using. All the above legend values cannot @@ -115,23 +117,23 @@ the graph crosses a particular threshold. ### Chart Options -- ``Bar`` - Display values as a bar chart -- ``Lines`` - Display values as a line graph -- ``Points`` - Display points for values +- **Bar** - Display values as a bar chart +- **Lines** - Display values as a line graph +- **Points** - Display points for values ### Line Options -- ``Line Fill`` - Amount of color fill for a series. 0 is none. -- ``Line Width`` - The width of the line for a series. -- ``Null point mode`` - How null values are displayed -- ``Staircase line`` - Draws adjacent points as staircase +- **Line Fill** - Amount of color fill for a series. 0 is none. +- **Line Width** - The width of the line for a series. +- **Null point mode** - How null values are displayed +- **Staircase line** - Draws adjacent points as staircase ### Multiple Series If there are multiple series, they can be displayed as a group. -- ``Stack`` - Each series is stacked on top of another -- ``Percent`` - Each series is drawn as a percentage of the total of all series +- **Stack** - Each series is stacked on top of another +- **Percent** - Each series is drawn as a percentage of the total of all series If you have stack enabled, you can select what the mouse hover feature should show. @@ -140,12 +142,12 @@ If you have stack enabled, you can select what the mouse hover feature should sh ### Rendering -- ``Flot`` - Render the graphs in the browser using Flot (default) -- ``Graphite PNG`` - Render the graph on the server using graphite's render API. +- **Flot** - Render the graphs in the browser using Flot (default) +- **Graphite PNG** - Render the graph on the server using graphite's render API. ### Tooltip -- ``All series`` - Show all series on the same tooltip and a x crosshairs to help follow all series +- **All series** - Show all series on the same tooltip and a x crosshairs to help follow all series ### Series Specific Overrides @@ -158,12 +160,6 @@ There is an option under Series overrides to draw lines as dashes. Set Dashes to ## Time Range -<<<<<<< HEAD The time range tab allows you to override the dashboard time range and specify a panel specific time. Either through a relative from now time option or through a timeshift. {{< docs-imagebox img="/img/docs/v45/graph-time-range.png" max-width= "900px" >}} -||||||| merged common ancestors -![](/img/docs/v2/graph_time_range.png) -======= - ->>>>>>> 0a65100eaf64cd57b38110001bf614630821610c diff --git a/docs/sources/features/panels/singlestat.md b/docs/sources/features/panels/singlestat.md index 5891848f579..5e2cb36600b 100644 --- a/docs/sources/features/panels/singlestat.md +++ b/docs/sources/features/panels/singlestat.md @@ -22,20 +22,20 @@ The singlestat panel has a normal query editor to allow you define your exact me {{< docs-imagebox img="/img/docs/v45/singlestat-value-options.png" class="docs-image--no-shadow" max-width="900px" >}} -1. `Stats`: The Stats field let you set the function (min, max, average, current, total, first, delta, range) that your entire query is reduced into a single value with. This reduces the entire query into a single summary value that is displayed. - * `min` - The smallest value in the series - * `max` - The largest value in the series - * `avg` - The average of all the non-null values in the series - * `current` - The last value in the series. If the series ends on null the previous value will be used. - * `total` - The sum of all the non-null values in the series - * `first` - The first value in the series - * `delta` - The total incremental increase (of a counter) in the series. An attempt is made to account for counter resets, but this will only be accurate for single instance metrics. Used to show total counter increase in time series. - * `diff` - The difference betwen 'current' (last value) and 'first'. - * `range` - The difference between 'min' and 'max'. Useful the show the range of change for a gauge. -2. `Prefix/Postfix`: The Prefix/Postfix fields let you define a custom label to appear *before/after* the value. The `$__name` variable can be used here to use the series name or alias from the metric query. -3. `Units`: Units are appended to the the Singlestat within the panel, and will respect the color and threshold settings for the value. -4. `Decimals`: The Decimal field allows you to override the automatic decimal precision, and set it explicitly. -5. `Font Size`: You can use this section to select the font size of the different texts in the Singlestat Panel, i.e. prefix, value and postfix. +1. **Stats**: The Stats field let you set the function (min, max, average, current, total, first, delta, range) that your entire query is reduced into a single value with. This reduces the entire query into a single summary value that is displayed. + * **min** - The smallest value in the series + * **max** - The largest value in the series + * **avg** - The average of all the non-null values in the series + * **current** - The last value in the series. If the series ends on null the previous value will be used. + * **total** - The sum of all the non-null values in the series + * **first** - The first value in the series + * **delta** - The total incremental increase (of a counter) in the series. An attempt is made to account for counter resets, but this will only be accurate for single instance metrics. Used to show total counter increase in time series. + * **diff** - The difference betwen 'current' (last value) and 'first'. + * **range** - The difference between 'min' and 'max'. Useful the show the range of change for a gauge. +2. **Prefix/Postfix**: The Prefix/Postfix fields let you define a custom label to appear *before/after* the value. The `$__name` variable can be used here to use the series name or alias from the metric query. +3. **Units**: Units are appended to the the Singlestat within the panel, and will respect the color and threshold settings for the value. +4. **Decimals**: The Decimal field allows you to override the automatic decimal precision, and set it explicitly. +5. **Font Size**: You can use this section to select the font size of the different texts in the Singlestat Panel, i.e. prefix, value and postfix. ### Coloring @@ -43,11 +43,11 @@ The coloring options of the Singlestat Panel config allow you to dynamically cha {{< docs-imagebox img="/img/docs/v45/singlestat-color-options.png" max-width="500px" class="docs-image--right docs-image--no-shadow">}} -1. `Background`: This checkbox applies the configured thresholds and colors to the entirety of the Singlestat Panel background. -2. `Thresholds`: Change the background and value colors dynamically within the panel, depending on the Singlestat value. The threshold field accepts **2 comma-separated** values which represent 3 ranges that correspond to the three colors directly to the right. For example: if the thresholds are 70, 90 then the first color represents < 70, the second color represents between 70 and 90 and the third color represents > 90. -3. `Colors`: Select a color and opacity -4. `Value`: This checkbox applies the configured thresholds and colors to the summary stat. -5. `Invert order`: This link toggles the threshold color order.
    For example: Green, Orange, Red () will become Red, Orange, Green (). +1. **Background**: This checkbox applies the configured thresholds and colors to the entirety of the Singlestat Panel background. +2. **Thresholds**: Change the background and value colors dynamically within the panel, depending on the Singlestat value. The threshold field accepts **2 comma-separated** values which represent 3 ranges that correspond to the three colors directly to the right. For example: if the thresholds are 70, 90 then the first color represents < 70, the second color represents between 70 and 90 and the third color represents > 90. +3. **Colors**: Select a color and opacity +4. **Value**: This checkbox applies the configured thresholds and colors to the summary stat. +5. **Invert order**: This link toggles the threshold color order.
    For example: Green, Orange, Red () will become Red, Orange, Green (). ### Spark Lines @@ -55,10 +55,10 @@ Sparklines are a great way of seeing the historical data related to the summary {{< docs-imagebox img="/img/docs/v45/singlestat-spark-options.png" max-width="500px" class="docs-image--right docs-image--no-shadow">}} -1. `Show`: The show checkbox will toggle whether the spark line is shown in the Panel. When unselected, only the Singlestat value will appear. -2. `Full Height`: Check if you want the sparklines to take up the full panel height, or uncheck if they should be below the main Singlestat value. -3. `Line Color`: This color selection applies to the color of the sparkline itself. -4. `Fill Color`: This color selection applies to the area below the sparkline. +1. **Show**: The show checkbox will toggle whether the spark line is shown in the Panel. When unselected, only the Singlestat value will appear. +2. **Full Height**: Check if you want the sparklines to take up the full panel height, or uncheck if they should be below the main Singlestat value. +3. **Line Color**: This color selection applies to the color of the sparkline itself. +4. **Fill Color**: This color selection applies to the area below the sparkline.
    @@ -70,10 +70,10 @@ Gauges gives a clear picture of how high a value is in it's context. It's a grea {{< docs-imagebox img="/img/docs/v45/singlestat-gauge-options.png" max-width="500px" class="docs-image--right docs-image--no-shadow">}} -1. `Show`: The show checkbox will toggle wether the gauge is shown in the panel. When unselected, only the Singlestat value will appear. -2. `Min/Max`: This sets the start and end point for the gauge. -3. `Threshold Labels`: Check if you want to show the threshold labels. Thresholds are set in the color options. -4. `Threshold Markers`: Check if you want to have a second meter showing the thresholds. +1. **Show**: The show checkbox will toggle wether the gauge is shown in the panel. When unselected, only the Singlestat value will appear. +2. **Min/Max**: This sets the start and end point for the gauge. +3. **Threshold Labels**: Check if you want to show the threshold labels. Thresholds are set in the color options. +4. **Threshold Markers**: Check if you want to have a second meter showing the thresholds.
    From ef518f6ecdaf15417e4a18751dfa84c669a7ca93 Mon Sep 17 00:00:00 2001 From: cglrkn Date: Tue, 3 Oct 2017 15:13:05 +0300 Subject: [PATCH 31/83] Build URL for close alert request differently --- pkg/services/alerting/notifiers/opsgenie.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/services/alerting/notifiers/opsgenie.go b/pkg/services/alerting/notifiers/opsgenie.go index 5af9fa69611..75ddfc3cf78 100644 --- a/pkg/services/alerting/notifiers/opsgenie.go +++ b/pkg/services/alerting/notifiers/opsgenie.go @@ -125,7 +125,7 @@ func (this *OpsGenieNotifier) closeAlert(evalContext *alerting.EvalContext) erro body, _ := bodyJSON.MarshalJSON() cmd := &m.SendWebhookSync{ - Url: fmt.Sprintf("%s/%s/close?identifierType=alias", opsgenieAlertURL,"alertId-"+strconv.FormatInt(evalContext.Rule.Id, 10)), + Url: fmt.Sprintf("%s/alertId-%d/close?identifierType=alias", opsgenieAlertURL, evalContext.Rule.Id) Body: string(body), HttpMethod: "POST", HttpHeader: map[string]string{ From a1b543aa3575d2b88614beb119750c200286ac66 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Tue, 3 Oct 2017 18:27:25 +0200 Subject: [PATCH 32/83] shore: cleanup unused stuff in common.d.ts --- public/app/core/utils/file_export.ts | 4 +- .../export_data/export_data_modal.ts | 4 +- public/app/headers/common.d.ts | 62 ------------------- .../datasource/elasticsearch/index_pattern.ts | 7 +-- 4 files changed, 5 insertions(+), 72 deletions(-) diff --git a/public/app/core/utils/file_export.ts b/public/app/core/utils/file_export.ts index 96af397fadf..173ac590ee7 100644 --- a/public/app/core/utils/file_export.ts +++ b/public/app/core/utils/file_export.ts @@ -1,11 +1,9 @@ -/// - import _ from 'lodash'; import moment from 'moment'; declare var window: any; -const DEFAULT_DATETIME_FORMAT: String = 'YYYY-MM-DDTHH:mm:ssZ'; +const DEFAULT_DATETIME_FORMAT = 'YYYY-MM-DDTHH:mm:ssZ'; export function exportSeriesListToCsv(seriesList, dateTimeFormat = DEFAULT_DATETIME_FORMAT, excel = false) { var text = excel ? 'sep=;\n' : '' + 'Series;Time;Value\n'; diff --git a/public/app/features/dashboard/export_data/export_data_modal.ts b/public/app/features/dashboard/export_data/export_data_modal.ts index f4bc5d3f2ea..da39611f901 100644 --- a/public/app/features/dashboard/export_data/export_data_modal.ts +++ b/public/app/features/dashboard/export_data/export_data_modal.ts @@ -1,5 +1,3 @@ -/// - import angular from 'angular'; import * as fileExport from 'app/core/utils/file_export'; import appEvents from 'app/core/app_events'; @@ -8,7 +6,7 @@ export class ExportDataModalCtrl { private data: any[]; private panel: string; asRows: Boolean = true; - dateTimeFormat: String = 'YYYY-MM-DDTHH:mm:ssZ'; + dateTimeFormat = 'YYYY-MM-DDTHH:mm:ssZ'; excel: false; export() { diff --git a/public/app/headers/common.d.ts b/public/app/headers/common.d.ts index dca80a3925e..94ae1b1abef 100644 --- a/public/app/headers/common.d.ts +++ b/public/app/headers/common.d.ts @@ -1,74 +1,12 @@ - declare var System: any; -// dummy modules -declare module 'app/core/config' { - var config: any; - export default config; -} - -declare module 'lodash' { - var lodash: any; - export default lodash; -} - -declare module 'moment' { - var moment: any; - export default moment; -} - -declare module 'angular' { - var angular: any; - export default angular; -} - -declare module 'jquery' { - var jquery: any; - export default jquery; -} - -declare module 'app/core/utils/kbn' { - var kbn: any; - export default kbn; -} - -declare module 'app/core/store' { - var store: any; - export default store; -} - -declare module 'tether' { - var config: any; - export default config; -} - -declare module 'tether-drop' { - var config: any; - export default config; -} - declare module 'eventemitter3' { var config: any; export default config; } -declare module 'mousetrap' { - var config: any; - export default config; -} - -declare module 'remarkable' { - var config: any; - export default config; -} - declare module 'd3' { var d3: any; export default d3; } -declare module 'ace' { - var ace: any; - export default ace; -} - diff --git a/public/app/plugins/datasource/elasticsearch/index_pattern.ts b/public/app/plugins/datasource/elasticsearch/index_pattern.ts index 4778dafa48f..65d80031875 100644 --- a/public/app/plugins/datasource/elasticsearch/index_pattern.ts +++ b/public/app/plugins/datasource/elasticsearch/index_pattern.ts @@ -1,5 +1,3 @@ -/// - import moment from 'moment'; const intervalMap = { @@ -29,10 +27,11 @@ export class IndexPattern { var intervalInfo = intervalMap[this.interval]; var start = moment(from).utc().startOf(intervalInfo.startOf); - var end = moment(to).utc().startOf(intervalInfo.startOf).valueOf(); + var startEpoch = start.valueOf(); + var endEpoch = moment(to).utc().startOf(intervalInfo.startOf).valueOf(); var indexList = []; - while (start <= end) { + while (startEpoch <= endEpoch) { indexList.push(start.format(this.pattern)); start.add(1, intervalInfo.amount); } From 0831238420810cac793f00ac3b8bbebddfcae0a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Tue, 3 Oct 2017 19:38:52 +0200 Subject: [PATCH 33/83] build: fixed broken elastic unit test --- public/app/plugins/datasource/elasticsearch/index_pattern.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/public/app/plugins/datasource/elasticsearch/index_pattern.ts b/public/app/plugins/datasource/elasticsearch/index_pattern.ts index 65d80031875..075c05dbf3f 100644 --- a/public/app/plugins/datasource/elasticsearch/index_pattern.ts +++ b/public/app/plugins/datasource/elasticsearch/index_pattern.ts @@ -27,11 +27,10 @@ export class IndexPattern { var intervalInfo = intervalMap[this.interval]; var start = moment(from).utc().startOf(intervalInfo.startOf); - var startEpoch = start.valueOf(); var endEpoch = moment(to).utc().startOf(intervalInfo.startOf).valueOf(); var indexList = []; - while (startEpoch <= endEpoch) { + while (start.valueOf() <= endEpoch) { indexList.push(start.format(this.pattern)); start.add(1, intervalInfo.amount); } From 1af00f5209a3bdfd89fcfdeea73d3178a0726214 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Tue, 3 Oct 2017 20:25:47 +0200 Subject: [PATCH 34/83] build: remove clean plugin from dev build --- scripts/webpack/webpack.dev.js | 1 - 1 file changed, 1 deletion(-) diff --git a/scripts/webpack/webpack.dev.js b/scripts/webpack/webpack.dev.js index ab48864b8a0..e852f966cf7 100644 --- a/scripts/webpack/webpack.dev.js +++ b/scripts/webpack/webpack.dev.js @@ -44,7 +44,6 @@ module.exports = merge(common, { new webpack.optimize.CommonsChunkPlugin({ names: ['vendor', 'manifest'], }), - new WebpackCleanupPlugin(), // new BundleAnalyzerPlugin({ // analyzerPort: 8889 // }) From 7859d4ffca6eca7df6f2ab1d40c78da2abaf3a38 Mon Sep 17 00:00:00 2001 From: Dan Cech Date: Wed, 4 Oct 2017 07:04:10 +0200 Subject: [PATCH 35/83] fix missing column headers in excel export (#9413) --- public/app/core/utils/file_export.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/public/app/core/utils/file_export.ts b/public/app/core/utils/file_export.ts index 173ac590ee7..a6070cb131c 100644 --- a/public/app/core/utils/file_export.ts +++ b/public/app/core/utils/file_export.ts @@ -6,7 +6,7 @@ declare var window: any; const DEFAULT_DATETIME_FORMAT = 'YYYY-MM-DDTHH:mm:ssZ'; export function exportSeriesListToCsv(seriesList, dateTimeFormat = DEFAULT_DATETIME_FORMAT, excel = false) { - var text = excel ? 'sep=;\n' : '' + 'Series;Time;Value\n'; + var text = (excel ? 'sep=;\n' : '') + 'Series;Time;Value\n'; _.each(seriesList, function(series) { _.each(series.datapoints, function(dp) { text += series.alias + ';' + moment(dp[1]).format(dateTimeFormat) + ';' + dp[0] + '\n'; @@ -16,7 +16,7 @@ export function exportSeriesListToCsv(seriesList, dateTimeFormat = DEFAULT_DATET } export function exportSeriesListToCsvColumns(seriesList, dateTimeFormat = DEFAULT_DATETIME_FORMAT, excel = false) { - var text = excel ? 'sep=;\n' : '' + 'Time;'; + var text = (excel ? 'sep=;\n' : '') + 'Time;'; // add header _.each(seriesList, function(series) { text += series.alias + ';'; From 2aae2556a5c765ba528b2ce46aec06dfa2f9106d Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Wed, 4 Oct 2017 08:15:23 +0300 Subject: [PATCH 36/83] Unified colorpicker (#9347) * colorpicker: initial picker with predefined palette and spectrum * colorpicker: highlight selected color * colorpicker: add onChange() callback * colorpicker: replace singlestat picker by new one * colorpicker: style tweak * colorpicker: parse color on input blur * colorpicker: sort palette by hue and lightness * colorpicker: refactor, move colors sorting to 'app/core/utils/colors' * tech: colorpicker - fix linter errors * colorpicker: convert to React components * colorpicker: fix spectrum import after moving to webpack * colorpicker: minor refactor * colorpicker: initial series color picker * colorpicker: fix tests error --- .../components/colorpicker/ColorPalette.tsx | 45 +++++++ .../components/colorpicker/ColorPicker.tsx | 81 +++++++++++ .../colorpicker/ColorPickerPopover.tsx | 127 ++++++++++++++++++ .../colorpicker/SeriesColorPicker.tsx | 49 +++++++ .../components/colorpicker/SpectrumPicker.tsx | 76 +++++++++++ public/app/core/core.ts | 2 + public/app/core/utils/colors.ts | 31 ++++- public/app/plugins/panel/graph/legend.js | 3 +- .../app/plugins/panel/singlestat/editor.html | 6 +- public/app/plugins/panel/singlestat/module.ts | 7 + public/sass/components/_color_picker.scss | 10 ++ 11 files changed, 432 insertions(+), 5 deletions(-) create mode 100644 public/app/core/components/colorpicker/ColorPalette.tsx create mode 100644 public/app/core/components/colorpicker/ColorPicker.tsx create mode 100644 public/app/core/components/colorpicker/ColorPickerPopover.tsx create mode 100644 public/app/core/components/colorpicker/SeriesColorPicker.tsx create mode 100644 public/app/core/components/colorpicker/SpectrumPicker.tsx diff --git a/public/app/core/components/colorpicker/ColorPalette.tsx b/public/app/core/components/colorpicker/ColorPalette.tsx new file mode 100644 index 00000000000..47e0e244e0a --- /dev/null +++ b/public/app/core/components/colorpicker/ColorPalette.tsx @@ -0,0 +1,45 @@ +import React from 'react'; +import coreModule from 'app/core/core_module'; +import { sortedColors } from 'app/core/utils/colors'; + +export interface IProps { + color: string; + onColorSelect: (c: string) => void; +} + +export class GfColorPalette extends React.Component { + paletteColors: string[]; + + constructor(props) { + super(props); + this.paletteColors = sortedColors; + this.onColorSelect = this.onColorSelect.bind(this); + } + + onColorSelect(color) { + return () => { + this.props.onColorSelect(color); + }; + } + + render() { + const colorPaletteItems = this.paletteColors.map((paletteColor) => { + const cssClass = paletteColor.toLowerCase() === this.props.color.toLowerCase() ? 'fa-circle-o' : 'fa-circle'; + return ( +   + + ); + }); + return ( +
    +

    {colorPaletteItems}

    +
    + ); + } +} + +coreModule.directive('gfColorPalette', function (reactDirective) { + return reactDirective(GfColorPalette, ['color', 'onColorSelect']); +}); diff --git a/public/app/core/components/colorpicker/ColorPicker.tsx b/public/app/core/components/colorpicker/ColorPicker.tsx new file mode 100644 index 00000000000..8ef51ce0be7 --- /dev/null +++ b/public/app/core/components/colorpicker/ColorPicker.tsx @@ -0,0 +1,81 @@ +import React from 'react'; +import ReactDOM from 'react-dom'; +import $ from 'jquery'; +import Drop from 'tether-drop'; +import coreModule from 'app/core/core_module'; +import { ColorPickerPopover } from './ColorPickerPopover'; + +export interface IProps { + color: string; + onChange: (c: string) => void; +} + +export class ColorPicker extends React.Component { + pickerElem: any; + colorPickerDrop: any; + + constructor(props) { + super(props); + this.openColorPicker = this.openColorPicker.bind(this); + this.closeColorPicker = this.closeColorPicker.bind(this); + this.setPickerElem = this.setPickerElem.bind(this); + this.onColorSelect = this.onColorSelect.bind(this); + } + + setPickerElem(elem) { + this.pickerElem = $(elem); + } + + openColorPicker() { + const dropContent = ( + + ); + + let dropContentElem = document.createElement('div'); + ReactDOM.render(dropContent, dropContentElem); + + let drop = new Drop({ + target: this.pickerElem[0], + content: dropContentElem, + position: 'top center', + classes: 'drop-popover drop-popover--form', + openOn: 'hover', + hoverCloseDelay: 200, + tetherOptions: { + constraints: [{ to: 'scrollParent', attachment: "none both" }] + } + }); + + drop.on('close', this.closeColorPicker); + + this.colorPickerDrop = drop; + this.colorPickerDrop.open(); + } + + closeColorPicker() { + setTimeout(() => { + if (this.colorPickerDrop && this.colorPickerDrop.tether) { + this.colorPickerDrop.destroy(); + } + }, 100); + } + + onColorSelect(color) { + this.props.onChange(color); + } + + render() { + return ( +
    +
    +
    +
    +
    +
    + ); + } +} + +coreModule.directive('colorPicker', function (reactDirective) { + return reactDirective(ColorPicker, ['color', 'onChange']); +}); diff --git a/public/app/core/components/colorpicker/ColorPickerPopover.tsx b/public/app/core/components/colorpicker/ColorPickerPopover.tsx new file mode 100644 index 00000000000..eaec3273507 --- /dev/null +++ b/public/app/core/components/colorpicker/ColorPickerPopover.tsx @@ -0,0 +1,127 @@ +import React from 'react'; +import $ from 'jquery'; +import coreModule from 'app/core/core_module'; +import { GfColorPalette } from './ColorPalette'; +import { GfSpectrumPicker } from './SpectrumPicker'; + +// Spectrum picker uses TinyColor and loads it as a global variable, so we can use it here also +declare var tinycolor; + +export interface IProps { + color: string; + onColorSelect: (c: string) => void; +} + +export class ColorPickerPopover extends React.Component { + pickerNavElem: any; + + constructor(props) { + super(props); + this.state = { + tab: 'palette', + color: this.props.color, + colorString: this.props.color + }; + + this.onColorStringChange = this.onColorStringChange.bind(this); + this.onColorStringBlur = this.onColorStringBlur.bind(this); + this.sampleColorSelected = this.sampleColorSelected.bind(this); + this.spectrumColorSelected = this.spectrumColorSelected.bind(this); + this.setPickerNavElem = this.setPickerNavElem.bind(this); + } + + setPickerNavElem(elem) { + this.pickerNavElem = $(elem); + } + + setColor(color) { + let newColor = tinycolor(color); + if (newColor.isValid()) { + this.setState({ + color: newColor.toString(), + colorString: newColor.toString() + }); + this.props.onColorSelect(color); + } + } + + sampleColorSelected(color) { + this.setColor(color); + } + + spectrumColorSelected(color) { + let rgbColor = color.toRgbString(); + this.setColor(rgbColor); + } + + onColorStringChange(e) { + let colorString = e.target.value; + this.setState({ + colorString: colorString + }); + + let newColor = tinycolor(colorString); + if (newColor.isValid()) { + // Update only color state + this.setState({ + color: newColor.toString(), + }); + this.props.onColorSelect(newColor); + } + } + + onColorStringBlur(e) { + let colorString = e.target.value; + this.setColor(colorString); + } + + componentDidMount() { + this.pickerNavElem.find('li:first').addClass('active'); + this.pickerNavElem.on('show', (e) => { + // use href attr (#name => name) + let tab = e.target.hash.slice(1); + this.setState({ + tab: tab + }); + }); + } + + render() { + const paletteTab = ( +
    + +
    + ); + const spectrumTab = ( +
    + +
    + ); + const currentTab = this.state.tab === 'palette' ? paletteTab : spectrumTab; + + return ( +
    + +
    + {currentTab} +
    +
    + + +
    +
    + ); + } +} + +coreModule.directive('gfColorPickerPopover', function (reactDirective) { + return reactDirective(ColorPickerPopover, ['color', 'onColorSelect']); +}); diff --git a/public/app/core/components/colorpicker/SeriesColorPicker.tsx b/public/app/core/components/colorpicker/SeriesColorPicker.tsx new file mode 100644 index 00000000000..6ea8b1710b3 --- /dev/null +++ b/public/app/core/components/colorpicker/SeriesColorPicker.tsx @@ -0,0 +1,49 @@ +import React from 'react'; +import coreModule from 'app/core/core_module'; +import { ColorPickerPopover } from './ColorPickerPopover'; + +export interface IProps { + series: any; + onColorChange: (color: string) => void; + onToggleAxis: () => void; +} + +export class SeriesColorPicker extends React.Component { + constructor(props) { + super(props); + this.onColorChange = this.onColorChange.bind(this); + this.onToggleAxis = this.onToggleAxis.bind(this); + } + + onColorChange(color) { + this.props.onColorChange(color); + } + + onToggleAxis() { + this.props.onToggleAxis(); + } + + render() { + const leftButtonClass = this.props.series.yaxis === 1 ? 'btn-success' : 'btn-inverse'; + const rightButtonClass = this.props.series.yaxis === 2 ? 'btn-success' : 'btn-inverse'; + + return ( +
    +
    + + + +
    + +
    + ); + } +} + +coreModule.directive('seriesColorPicker', function (reactDirective) { + return reactDirective(SeriesColorPicker, ['series', 'onColorChange', 'onToggleAxis']); +}); diff --git a/public/app/core/components/colorpicker/SpectrumPicker.tsx b/public/app/core/components/colorpicker/SpectrumPicker.tsx new file mode 100644 index 00000000000..2ea0fc65ddf --- /dev/null +++ b/public/app/core/components/colorpicker/SpectrumPicker.tsx @@ -0,0 +1,76 @@ +import React from 'react'; +import coreModule from 'app/core/core_module'; +import _ from 'lodash'; +import $ from 'jquery'; +import 'vendor/spectrum'; + +export interface IProps { + color: string; + options: object; + onColorSelect: (c: string) => void; +} + +export class GfSpectrumPicker extends React.Component { + elem: any; + isMoving: boolean; + + constructor(props) { + super(props); + this.onSpectrumMove = this.onSpectrumMove.bind(this); + this.setComponentElem = this.setComponentElem.bind(this); + } + + setComponentElem(elem) { + this.elem = $(elem); + } + + onSpectrumMove(color) { + this.isMoving = true; + this.props.onColorSelect(color); + } + + componentDidMount() { + 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'); + this.elem.spectrum('set', this.props.color); + } + + componentWillUpdate(nextProps) { + // If user move pointer over spectrum field this produce 'move' event and component + // may update props.color. We don't want to update spectrum color in this case, so we can use + // isMoving flag for tracking moving state. Flag should be cleared in componentDidUpdate() which + // is called after updating occurs (when user finished moving). + if (!this.isMoving) { + this.elem.spectrum('set', nextProps.color); + } + } + + componentDidUpdate() { + if (this.isMoving) { + this.isMoving = false; + } + } + + componentWillUnmount() { + this.elem.spectrum('destroy'); + } + + render() { + return ( +
    + ); + } +} + +coreModule.directive('gfSpectrumPicker', function (reactDirective) { + return reactDirective(GfSpectrumPicker, ['color', 'options', 'onColorSelect']); +}); diff --git a/public/app/core/core.ts b/public/app/core/core.ts index 675b9eba2fb..9ac3fd8c2a2 100644 --- a/public/app/core/core.ts +++ b/public/app/core/core.ts @@ -16,6 +16,8 @@ import './partials'; import './components/jsontree/jsontree'; import './components/code_editor/code_editor'; import './utils/outline'; +import './components/colorpicker/ColorPicker'; +import './components/colorpicker/SeriesColorPicker'; import {grafanaAppDirective} from './components/grafana_app'; import {sideMenuDirective} from './components/sidemenu/sidemenu'; diff --git a/public/app/core/utils/colors.ts b/public/app/core/utils/colors.ts index bd774ea02ea..b9dd164d635 100644 --- a/public/app/core/utils/colors.ts +++ b/public/app/core/utils/colors.ts @@ -1,6 +1,12 @@ +import _ from 'lodash'; +// Spectrum picker uses TinyColor and loads it as a global variable, so we can use it here also +declare var tinycolor; -export default [ +export const PALETTE_ROWS = 4; +export const PALETTE_COLUMNS = 14; + +let colors = [ "#7EB26D","#EAB839","#6ED0E0","#EF843C","#E24D42","#1F78C1","#BA43A9","#705DA0", "#508642","#CCA300","#447EBC","#C15C17","#890F02","#0A437C","#6D1F62","#584477", "#B7DBAB","#F4D598","#70DBED","#F9BA8F","#F29191","#82B5D8","#E5A8E2","#AEA2E0", @@ -10,3 +16,26 @@ export default [ "#E0F9D7","#FCEACA","#CFFAFF","#F9E2D2","#FCE2DE","#BADFF4","#F9D9F9","#DEDAF7" ]; +export function sortColorsByHue(hexColors) { + let hslColors = _.map(hexColors, hexToHsl); + + let sortedHSLColors = _.sortBy(hslColors, ['h']); + sortedHSLColors = _.chunk(sortedHSLColors, PALETTE_ROWS); + sortedHSLColors = _.map(sortedHSLColors, chunk => { + return _.sortBy(chunk, 'l'); + }); + sortedHSLColors = _.flattenDeep(_.zip(...sortedHSLColors)); + + return _.map(sortedHSLColors, hslToHex); +} + +export function hexToHsl(color) { + return tinycolor(color).toHsl(); +} + +export function hslToHex(color) { + return tinycolor(color).toHexString(); +} + +export let sortedColors = sortColorsByHue(colors); +export default colors; diff --git a/public/app/plugins/panel/graph/legend.js b/public/app/plugins/panel/graph/legend.js index beee24543d0..b8c2970b8cd 100644 --- a/public/app/plugins/panel/graph/legend.js +++ b/public/app/plugins/panel/graph/legend.js @@ -45,7 +45,8 @@ function (angular, _, $) { popoverSrv.show({ element: el[0], position: 'bottom center', - template: '', + template: '' + + '', openOn: 'hover', model: { series: series, diff --git a/public/app/plugins/panel/singlestat/editor.html b/public/app/plugins/panel/singlestat/editor.html index 6ec8705dbe3..1981b4357f8 100644 --- a/public/app/plugins/panel/singlestat/editor.html +++ b/public/app/plugins/panel/singlestat/editor.html @@ -69,13 +69,13 @@
    - + - + - + diff --git a/public/app/plugins/panel/singlestat/module.ts b/public/app/plugins/panel/singlestat/module.ts index 7fbd3068375..b4ee2bf7fb7 100644 --- a/public/app/plugins/panel/singlestat/module.ts +++ b/public/app/plugins/panel/singlestat/module.ts @@ -214,6 +214,13 @@ class SingleStatCtrl extends MetricsPanelCtrl { this.render(); } + onColorChange(panelColorIndex) { + return (color) => { + this.panel.colors[panelColorIndex] = color; + this.render(); + }; + } + getDecimalsForValue(value) { if (_.isNumber(this.panel.decimals)) { return {decimals: this.panel.decimals, scaledDecimals: null}; diff --git a/public/sass/components/_color_picker.scss b/public/sass/components/_color_picker.scss index d5f04265fc8..15c5db61794 100644 --- a/public/sass/components/_color_picker.scss +++ b/public/sass/components/_color_picker.scss @@ -35,3 +35,13 @@ float: left; z-index: 0; } + +.colorpicker-container { + min-height: 190px; +} + +.drop-popover.gf-color-picker { + .drop-content { + width: 210px; + } +} From be3c5d1355d4254558e35e4b4688efa93682e1da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Wed, 4 Oct 2017 07:56:41 +0200 Subject: [PATCH 37/83] colorpicker: refactoring the new unififed colorpicker, #9347 --- public/app/core/components/colorpicker.ts | 69 ------------------- .../colorpicker/ColorPickerPopover.tsx | 22 +++--- .../colorpicker/SeriesColorPicker.tsx | 32 +++++---- public/app/core/core.ts | 2 - .../panel/graph/series_overrides_ctrl.js | 2 +- public/sass/components/_color_picker.scss | 5 +- public/sass/components/_gf-form.scss | 6 ++ 7 files changed, 37 insertions(+), 101 deletions(-) delete mode 100644 public/app/core/components/colorpicker.ts diff --git a/public/app/core/components/colorpicker.ts b/public/app/core/components/colorpicker.ts deleted file mode 100644 index f64903ad0b6..00000000000 --- a/public/app/core/components/colorpicker.ts +++ /dev/null @@ -1,69 +0,0 @@ -/// - -import coreModule from 'app/core/core_module'; - -var template = ` -
    -
    - - - -
    - -

    -   -

    -
    -`; - -export class ColorPickerCtrl { - colors: any; - autoClose: boolean; - series: any; - showAxisControls: boolean; - - /** @ngInject */ - constructor(private $scope, $rootScope) { - this.colors = $rootScope.colors; - this.autoClose = $scope.autoClose; - this.series = $scope.series; - } - - toggleAxis(yaxis) { - this.$scope.toggleAxis(); - - if (this.$scope.autoClose) { - this.$scope.dismiss(); - } - } - - colorSelected(color) { - this.$scope.colorSelected(color); - if (this.$scope.autoClose) { - this.$scope.dismiss(); - } - } -} - -export function colorPicker() { - return { - restrict: 'E', - controller: ColorPickerCtrl, - bindToController: true, - controllerAs: 'ctrl', - template: template, - }; -} - -coreModule.directive('gfColorPicker', colorPicker); diff --git a/public/app/core/components/colorpicker/ColorPickerPopover.tsx b/public/app/core/components/colorpicker/ColorPickerPopover.tsx index eaec3273507..49e1b1e2105 100644 --- a/public/app/core/components/colorpicker/ColorPickerPopover.tsx +++ b/public/app/core/components/colorpicker/ColorPickerPopover.tsx @@ -22,12 +22,6 @@ export class ColorPickerPopover extends React.Component { color: this.props.color, colorString: this.props.color }; - - this.onColorStringChange = this.onColorStringChange.bind(this); - this.onColorStringBlur = this.onColorStringBlur.bind(this); - this.sampleColorSelected = this.sampleColorSelected.bind(this); - this.spectrumColorSelected = this.spectrumColorSelected.bind(this); - this.setPickerNavElem = this.setPickerNavElem.bind(this); } setPickerNavElem(elem) { @@ -89,32 +83,32 @@ export class ColorPickerPopover extends React.Component { render() { const paletteTab = (
    - +
    ); const spectrumTab = (
    - +
    ); const currentTab = this.state.tab === 'palette' ? paletteTab : spectrumTab; return (
    -
      + -
      +
      {currentTab}
      -
      - +
      +
      diff --git a/public/app/core/components/colorpicker/SeriesColorPicker.tsx b/public/app/core/components/colorpicker/SeriesColorPicker.tsx index 6ea8b1710b3..e7294aa6281 100644 --- a/public/app/core/components/colorpicker/SeriesColorPicker.tsx +++ b/public/app/core/components/colorpicker/SeriesColorPicker.tsx @@ -1,6 +1,6 @@ import React from 'react'; import coreModule from 'app/core/core_module'; -import { ColorPickerPopover } from './ColorPickerPopover'; +import {ColorPickerPopover} from './ColorPickerPopover'; export interface IProps { series: any; @@ -23,27 +23,33 @@ export class SeriesColorPicker extends React.Component { this.props.onToggleAxis(); } - render() { + renderAxisSelection() { const leftButtonClass = this.props.series.yaxis === 1 ? 'btn-success' : 'btn-inverse'; const rightButtonClass = this.props.series.yaxis === 2 ? 'btn-success' : 'btn-inverse'; + return ( +
      + + + +
      + ); + } + + render() { return (
      -
      - - - -
      - + {this.props.series && this.renderAxisSelection()} +
      ); } } -coreModule.directive('seriesColorPicker', function (reactDirective) { +coreModule.directive('seriesColorPicker', function(reactDirective) { return reactDirective(SeriesColorPicker, ['series', 'onColorChange', 'onToggleAxis']); }); diff --git a/public/app/core/core.ts b/public/app/core/core.ts index 9ac3fd8c2a2..95b2e20aab6 100644 --- a/public/app/core/core.ts +++ b/public/app/core/core.ts @@ -23,7 +23,6 @@ import {grafanaAppDirective} from './components/grafana_app'; import {sideMenuDirective} from './components/sidemenu/sidemenu'; import {searchDirective} from './components/search/search'; import {infoPopover} from './components/info_popover'; -import {colorPicker} from './components/colorpicker'; import {navbarDirective} from './components/navbar/navbar'; import {arrayJoin} from './directives/array_join'; import {liveSrv} from './live/live_srv'; @@ -55,7 +54,6 @@ export { sideMenuDirective, navbarDirective, searchDirective, - colorPicker, liveSrv, layoutSelector, switchDirective, diff --git a/public/app/plugins/panel/graph/series_overrides_ctrl.js b/public/app/plugins/panel/graph/series_overrides_ctrl.js index 945f8340b33..2df993ff70e 100644 --- a/public/app/plugins/panel/graph/series_overrides_ctrl.js +++ b/public/app/plugins/panel/graph/series_overrides_ctrl.js @@ -57,7 +57,7 @@ define([ element: $element.find(".dropdown")[0], position: 'top center', openOn: 'click', - template: '', + template: '', model: { autoClose: true, colorSelected: $scope.colorSelected, diff --git a/public/sass/components/_color_picker.scss b/public/sass/components/_color_picker.scss index 15c5db61794..22c96398160 100644 --- a/public/sass/components/_color_picker.scss +++ b/public/sass/components/_color_picker.scss @@ -36,8 +36,9 @@ z-index: 0; } -.colorpicker-container { - min-height: 190px; +.gf-color-picker__body { + padding-bottom: 10px; + padding-left: 6px; } .drop-popover.gf-color-picker { diff --git a/public/sass/components/_gf-form.scss b/public/sass/components/_gf-form.scss index 14c8220337a..a44bb39b0e2 100644 --- a/public/sass/components/_gf-form.scss +++ b/public/sass/components/_gf-form.scss @@ -171,6 +171,12 @@ $gf-form-margin: 0.25rem; pointer-events: none; } } + + &--small { + padding-top: 4px; + padding-bottom: 4px; + font-size: $font-size-sm; + } } .gf-form-hint { From 548bc6b7c4af31567150b1544e219aea35a62039 Mon Sep 17 00:00:00 2001 From: cglrkn Date: Wed, 4 Oct 2017 10:15:39 +0300 Subject: [PATCH 38/83] Add the missing comma --- pkg/services/alerting/notifiers/opsgenie.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/services/alerting/notifiers/opsgenie.go b/pkg/services/alerting/notifiers/opsgenie.go index 75ddfc3cf78..9cc11da674b 100644 --- a/pkg/services/alerting/notifiers/opsgenie.go +++ b/pkg/services/alerting/notifiers/opsgenie.go @@ -125,7 +125,7 @@ func (this *OpsGenieNotifier) closeAlert(evalContext *alerting.EvalContext) erro body, _ := bodyJSON.MarshalJSON() cmd := &m.SendWebhookSync{ - Url: fmt.Sprintf("%s/alertId-%d/close?identifierType=alias", opsgenieAlertURL, evalContext.Rule.Id) + Url: fmt.Sprintf("%s/alertId-%d/close?identifierType=alias", opsgenieAlertURL, evalContext.Rule.Id), Body: string(body), HttpMethod: "POST", HttpHeader: map[string]string{ From 3c1beb1bddc7d90022fcecbd0852cf98f702f502 Mon Sep 17 00:00:00 2001 From: Daniel Lee Date: Wed, 4 Oct 2017 09:41:22 +0200 Subject: [PATCH 39/83] plugin_loader: expose app_events to plugins --- public/app/features/plugins/plugin_loader.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/public/app/features/plugins/plugin_loader.ts b/public/app/features/plugins/plugin_loader.ts index 8df27c0f320..89df75dc78e 100644 --- a/public/app/features/plugins/plugin_loader.ts +++ b/public/app/features/plugins/plugin_loader.ts @@ -8,6 +8,7 @@ import jquery from 'jquery'; import config from 'app/core/config'; import TimeSeries from 'app/core/time_series2'; import TableModel from 'app/core/table_model'; +import appEvents from 'app/core/app_events'; import {Observable} from 'rxjs/Observable'; import {Subject} from 'rxjs/Subject'; import * as datemath from 'app/core/utils/datemath'; @@ -57,6 +58,7 @@ exposeToPlugin('app/core/config', config); exposeToPlugin('app/core/time_series', TimeSeries); exposeToPlugin('app/core/time_series2', TimeSeries); exposeToPlugin('app/core/table_model', TableModel); +exposeToPlugin('app/core/app_events', appEvents); import 'vendor/flot/jquery.flot'; import 'vendor/flot/jquery.flot.selection'; From 0c31c7b10619088784bd28b31a4948e43d05d2bf Mon Sep 17 00:00:00 2001 From: bergquist Date: Wed, 4 Oct 2017 10:42:34 +0200 Subject: [PATCH 40/83] changelog: add note about closing #7175 --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 23633bfbc1e..b40f20fde0f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,7 @@ * **Dataproxy**: Allow grafan to renegotiate tls connection [#9250](https://github.com/grafana/grafana/issues/9250) * **HTTP**: set net.Dialer.DualStack to true for all http clients [#9367](https://github.com/grafana/grafana/pull/9367) * **Alerting**: Add diff and percent diff as series reducers [#9386](https://github.com/grafana/grafana/pull/9386), thx [@shanhuhai5739](https://github.com/shanhuhai5739) +* **Slack**: Allow images to be uploaded to slack when Token is precent [#7175](https://github.com/grafana/grafana/issues/7175), thx [@xginn8](https://github.com/xginn8) ## Tech * **Go**: Grafana is now built using golang 1.9 From 080c46f835fe00054074759ff18a382c4d2e2d7a Mon Sep 17 00:00:00 2001 From: Alin Sinpalean Date: Wed, 4 Oct 2017 15:30:07 +0200 Subject: [PATCH 41/83] Address review comments. --- .../datasource/prometheus/datasource.ts | 78 ++++++++----------- 1 file changed, 31 insertions(+), 47 deletions(-) diff --git a/public/app/plugins/datasource/prometheus/datasource.ts b/public/app/plugins/datasource/prometheus/datasource.ts index c0bb3079290..e7b7cf36aa0 100644 --- a/public/app/plugins/datasource/prometheus/datasource.ts +++ b/public/app/plugins/datasource/prometheus/datasource.ts @@ -1,15 +1,12 @@ /// import _ from 'lodash'; -import moment from 'moment'; import kbn from 'app/core/utils/kbn'; import * as dateMath from 'app/core/utils/datemath'; import PrometheusMetricFindQuery from './metric_find_query'; import TableModel from 'app/core/table_model'; -var durationSplitRegexp = /(\d+)(ms|s|m|h|d|w|M|y)/; - function prometheusSpecialRegexEscape(value) { return value.replace(/[\\^$*+?.()|[\]{}]/g, '\\\\$&'); } @@ -83,6 +80,7 @@ export class PrometheusDatasource { var self = this; var start = this.getPrometheusTime(options.range.from, false); var end = this.getPrometheusTime(options.range.to, true); + var range = Math.ceil(end - start); var queries = []; var activeTargets = []; @@ -95,33 +93,7 @@ export class PrometheusDatasource { } activeTargets.push(target); - - var query: any = {}; - query.instant = target.instant; - - var interval = this.intervalSeconds(options.interval); - // Minimum interval ("Min step"), if specified for the query. or same as interval otherwise - var minInterval = this.intervalSeconds(this.templateSrv.replace(target.interval, options.scopedVars) || options.interval); - var intervalFactor = target.intervalFactor || 1; - var range = Math.ceil(end - start); - // Adjust the interval to take into account any specified minimum and interval factor plus Prometheus limits - var adjustedInterval = this.adjustInterval(interval, minInterval, range, intervalFactor); - - var scopedVars = options.scopedVars; - // If the interval was adjusted, make a shallow copy of scopedVars with updated interval vars - if (interval !== adjustedInterval) { - interval = adjustedInterval; - scopedVars = Object.assign({}, options.scopedVars, { - "__interval": {text: interval + "s", value: interval + "s"}, - "__interval_ms": {text: interval * 1000, value: interval * 1000}, - }); - } - target.step = query.step = interval; - - // Only replace vars in expression after having (possibly) updated interval vars - query.expr = this.templateSrv.replace(target.expr, scopedVars, self.interpolateQueryExpr); - query.requestId = options.panelId + target.refId; - queries.push(query); + queries.push(this.createQuery(target, options, range)); } // No valid targets, return the empty result to save a round trip. @@ -162,14 +134,41 @@ export class PrometheusDatasource { }); } + createQuery(target, options, range) { + var query: any = {}; + query.instant = target.instant; + + var interval = kbn.interval_to_seconds(options.interval); + // Minimum interval ("Min step"), if specified for the query. or same as interval otherwise + var minInterval = kbn.interval_to_seconds(this.templateSrv.replace(target.interval, options.scopedVars) || options.interval); + var intervalFactor = target.intervalFactor || 1; + // Adjust the interval to take into account any specified minimum and interval factor plus Prometheus limits + var adjustedInterval = this.adjustInterval(interval, minInterval, range, intervalFactor); + + var scopedVars = options.scopedVars; + // If the interval was adjusted, make a shallow copy of scopedVars with updated interval vars + if (interval !== adjustedInterval) { + interval = adjustedInterval; + scopedVars = Object.assign({}, options.scopedVars, { + "__interval": {text: interval + "s", value: interval + "s"}, + "__interval_ms": {text: interval * 1000, value: interval * 1000}, + }); + } + target.step = query.step = interval; + + // Only replace vars in expression after having (possibly) updated interval vars + query.expr = this.templateSrv.replace(target.expr, scopedVars, this.interpolateQueryExpr); + query.requestId = options.panelId + target.refId; + return query; + } + adjustInterval(interval, minInterval, range, intervalFactor) { // Prometheus will drop queries that might return more than 11000 data points. // Calibrate interval if it is too small. if (interval !== 0 && range / intervalFactor / interval > 11000) { interval = Math.ceil(range / intervalFactor / 11000); } - interval = Math.max(interval * intervalFactor, minInterval); - return interval; + return Math.max(interval * intervalFactor, minInterval); } performTimeSeriesQuery(query, start, end) { @@ -273,21 +272,6 @@ export class PrometheusDatasource { }); } - calculateInterval(interval, intervalFactor) { - return Math.ceil(this.intervalSeconds(interval) * intervalFactor); - } - - intervalSeconds(interval) { - var m = interval.match(durationSplitRegexp); - var dur = moment.duration(parseInt(m[1]), m[2]); - var sec = dur.asSeconds(); - if (sec < 1) { - sec = 1; - } - - return sec; - } - transformMetricData(md, options, start, end) { var dps = [], metricLabel = null; From 7284db0e5494e7ba423ad188cec953951ded637e Mon Sep 17 00:00:00 2001 From: Joseph Weigl Date: Wed, 4 Oct 2017 16:07:13 +0200 Subject: [PATCH 42/83] Fix empty message and toolong attribute names Use default state message if no message is provided by the user Slice attribute name to maximum of 50 chars --- pkg/services/alerting/notifiers/hipchat.go | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/pkg/services/alerting/notifiers/hipchat.go b/pkg/services/alerting/notifiers/hipchat.go index 757120db25b..c131b80c578 100644 --- a/pkg/services/alerting/notifiers/hipchat.go +++ b/pkg/services/alerting/notifiers/hipchat.go @@ -86,8 +86,12 @@ func (this *HipChatNotifier) Notify(evalContext *alerting.EvalContext) error { attributes := make([]map[string]interface{}, 0) for index, evt := range evalContext.EvalMatches { + metricName := evt.Metric + if len(metricName) > 50 { + metricName = metricName[:50] + } attributes = append(attributes, map[string]interface{}{ - "label": evt.Metric, + "label": metricName, "value": map[string]interface{}{ "label": strconv.FormatFloat(evt.Value.Float64, 'f', -1, 64), }, @@ -110,6 +114,11 @@ func (this *HipChatNotifier) Notify(evalContext *alerting.EvalContext) error { if evalContext.Rule.State != models.AlertStateOK { //dont add message when going back to alert state ok. message += " " + evalContext.Rule.Message } + + if len(message) < 1 { + message = evalContext.GetNotificationTitle() + " in state " + evalContext.GetStateModel().Text + } + //HipChat has a set list of colors var color string switch evalContext.Rule.State { @@ -153,6 +162,7 @@ func (this *HipChatNotifier) Notify(evalContext *alerting.EvalContext) error { hipUrl := fmt.Sprintf("%s/v2/room/%s/notification?auth_token=%s", this.Url, this.RoomId, this.ApiKey) data, _ := json.Marshal(&body) + this.log.Debug(string(data)) cmd := &models.SendWebhookSync{Url: hipUrl, Body: string(data)} if err := bus.DispatchCtx(evalContext.Ctx, cmd); err != nil { From 9cf7a2d2ed42b746c9730461470a2d2599cfc2ee Mon Sep 17 00:00:00 2001 From: Alin Sinpalean Date: Wed, 4 Oct 2017 16:23:24 +0200 Subject: [PATCH 43/83] Remove apparently unnecessary .flush() calls. --- .../datasource/prometheus/specs/datasource_specs.ts | 8 -------- 1 file changed, 8 deletions(-) diff --git a/public/app/plugins/datasource/prometheus/specs/datasource_specs.ts b/public/app/plugins/datasource/prometheus/specs/datasource_specs.ts index a6e7f480f88..702ad8b5990 100644 --- a/public/app/plugins/datasource/prometheus/specs/datasource_specs.ts +++ b/public/app/plugins/datasource/prometheus/specs/datasource_specs.ts @@ -278,10 +278,6 @@ describe('PrometheusDatasource', function() { } }; - beforeEach(function() { - ctx.$httpBackend.flush(); - }); - it('should be min interval when greater than auto interval', function() { var query = { // 6 hour range @@ -420,10 +416,6 @@ describe('PrometheusDatasource', function() { } }; - beforeEach(function() { - ctx.$httpBackend.flush(); - }); - it('should be unchanged when auto interval is greater than min interval', function() { var query = { // 6 hour range From e4541a7fd1c4aa7abc0d4abbee27c8dcb045b48c Mon Sep 17 00:00:00 2001 From: Ricard Clau Date: Wed, 4 Oct 2017 21:04:20 +0200 Subject: [PATCH 44/83] support for s3 path (#9151) --- conf/defaults.ini | 1 + conf/sample.ini | 1 + docs/sources/installation/configuration.md | 5 +++++ pkg/components/imguploader/imguploader.go | 8 +++++++- pkg/components/imguploader/s3uploader.go | 6 ++++-- 5 files changed, 18 insertions(+), 3 deletions(-) diff --git a/conf/defaults.ini b/conf/defaults.ini index dfa6bf99017..14e77449241 100644 --- a/conf/defaults.ini +++ b/conf/defaults.ini @@ -479,6 +479,7 @@ provider = bucket_url = bucket = region = +path = access_key = secret_key = diff --git a/conf/sample.ini b/conf/sample.ini index 89170000df6..1aedfbf6532 100644 --- a/conf/sample.ini +++ b/conf/sample.ini @@ -424,6 +424,7 @@ [external_image_storage.s3] ;bucket = ;region = +;path = ;access_key = ;secret_key = diff --git a/docs/sources/installation/configuration.md b/docs/sources/installation/configuration.md index 85ff1026c10..6630825e803 100644 --- a/docs/sources/installation/configuration.md +++ b/docs/sources/installation/configuration.md @@ -651,11 +651,16 @@ These options control how images should be made public so they can be shared on You can choose between (s3, webdav, gcs). If left empty Grafana will ignore the upload action. ## [external_image_storage.s3] + ### bucket Bucket name for S3. e.g. grafana.snapshot + ### region Region name for S3. e.g. 'us-east-1', 'cn-north-1', etc +### path +Optional extra path inside bucket, useful to apply expiration policies + ### bucket_url (for backward compatibility, only works when no bucket or region are configured) Bucket URL for S3. AWS region can be specified within URL or defaults to 'us-east-1', e.g. diff --git a/pkg/components/imguploader/imguploader.go b/pkg/components/imguploader/imguploader.go index 1feeb6f07c3..728614735d0 100644 --- a/pkg/components/imguploader/imguploader.go +++ b/pkg/components/imguploader/imguploader.go @@ -30,9 +30,15 @@ func NewImageUploader() (ImageUploader, error) { bucket := s3sec.Key("bucket").MustString("") region := s3sec.Key("region").MustString("") + path := s3sec.Key("path").MustString("") bucketUrl := s3sec.Key("bucket_url").MustString("") accessKey := s3sec.Key("access_key").MustString("") secretKey := s3sec.Key("secret_key").MustString("") + + if path != "" && path[len(path)-1:] != "/" { + path += "/" + } + if bucket == "" || region == "" { info, err := getRegionAndBucketFromUrl(bucketUrl) if err != nil { @@ -42,7 +48,7 @@ func NewImageUploader() (ImageUploader, error) { region = info.region } - return NewS3Uploader(region, bucket, "public-read", accessKey, secretKey), nil + return NewS3Uploader(region, bucket, path, "public-read", accessKey, secretKey), nil case "webdav": webdavSec, err := setting.Cfg.GetSection("external_image_storage.webdav") if err != nil { diff --git a/pkg/components/imguploader/s3uploader.go b/pkg/components/imguploader/s3uploader.go index 860bb1a1abd..62196357c61 100644 --- a/pkg/components/imguploader/s3uploader.go +++ b/pkg/components/imguploader/s3uploader.go @@ -19,16 +19,18 @@ import ( type S3Uploader struct { region string bucket string + path string acl string secretKey string accessKey string log log.Logger } -func NewS3Uploader(region, bucket, acl, accessKey, secretKey string) *S3Uploader { +func NewS3Uploader(region, bucket, path, acl, accessKey, secretKey string) *S3Uploader { return &S3Uploader{ region: region, bucket: bucket, + path: path, acl: acl, accessKey: accessKey, secretKey: secretKey, @@ -56,7 +58,7 @@ func (u *S3Uploader) Upload(ctx context.Context, imageDiskPath string) (string, } s3_endpoint, _ := endpoints.DefaultResolver().EndpointFor("s3", u.region) - key := util.GetRandomString(20) + ".png" + key := u.path + util.GetRandomString(20) + ".png" image_url := s3_endpoint.URL + "/" + u.bucket + "/" + key log.Debug("Uploading image to s3", "url = ", image_url) From f8719efb67c138a4aa6c948b2c0fc1774e5bb6b0 Mon Sep 17 00:00:00 2001 From: Tomofumi Hayashi Date: Thu, 5 Oct 2017 13:33:44 +0900 Subject: [PATCH 45/83] Add milliseconds format in table panel's config This changes introduce milliseconds format option in table panel config GUI. Current grafana support milliseconds in time and actually used at graph panel, however current table does not provide the way to show milliseconds. This fix is to add format in table panel to show milliseconds in table as well as graph. --- public/app/plugins/panel/table/column_options.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/public/app/plugins/panel/table/column_options.ts b/public/app/plugins/panel/table/column_options.ts index 2191de2bb8d..c95382804c3 100644 --- a/public/app/plugins/panel/table/column_options.ts +++ b/public/app/plugins/panel/table/column_options.ts @@ -40,6 +40,7 @@ export class ColumnOptionsCtrl { this.fontSizes = ['80%', '90%', '100%', '110%', '120%', '130%', '150%', '160%', '180%', '200%', '220%', '250%']; this.dateFormats = [ {text: 'YYYY-MM-DD HH:mm:ss', value: 'YYYY-MM-DD HH:mm:ss'}, + {text: 'YYYY-MM-DD HH:mm:ss.SSS', value: 'YYYY-MM-DD HH:mm:ss.SSS'}, {text: 'MM/DD/YY h:mm:ss a', value: 'MM/DD/YY h:mm:ss a'}, {text: 'MMMM D, YYYY LT', value: 'MMMM D, YYYY LT'}, ]; From 5a3f32521ea26c0275aac8ecae233a8dd0834112 Mon Sep 17 00:00:00 2001 From: Cagla Arikan Date: Thu, 5 Oct 2017 08:42:45 +0300 Subject: [PATCH 46/83] Fix formatting issue --- pkg/services/alerting/notifiers/opsgenie.go | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/pkg/services/alerting/notifiers/opsgenie.go b/pkg/services/alerting/notifiers/opsgenie.go index 9cc11da674b..aea02465177 100644 --- a/pkg/services/alerting/notifiers/opsgenie.go +++ b/pkg/services/alerting/notifiers/opsgenie.go @@ -104,10 +104,10 @@ func (this *OpsGenieNotifier) createAlert(evalContext *alerting.EvalContext) err Url: opsgenieAlertURL, Body: string(body), HttpMethod: "POST", - HttpHeader: map[string]string{ - "Content-Type": "application/json", - "Authorization": fmt.Sprintf("GenieKey %s", this.ApiKey), - }, + HttpHeader: map[string]string{ + "Content-Type": "application/json", + "Authorization": fmt.Sprintf("GenieKey %s", this.ApiKey), + }, } if err := bus.DispatchCtx(evalContext.Ctx, cmd); err != nil { @@ -128,10 +128,10 @@ func (this *OpsGenieNotifier) closeAlert(evalContext *alerting.EvalContext) erro Url: fmt.Sprintf("%s/alertId-%d/close?identifierType=alias", opsgenieAlertURL, evalContext.Rule.Id), Body: string(body), HttpMethod: "POST", - HttpHeader: map[string]string{ - "Content-Type": "application/json", - "Authorization": fmt.Sprintf("GenieKey %s", this.ApiKey), - }, + HttpHeader: map[string]string{ + "Content-Type": "application/json", + "Authorization": fmt.Sprintf("GenieKey %s", this.ApiKey), + }, } if err := bus.DispatchCtx(evalContext.Ctx, cmd); err != nil { From 70f102dad494ea92998182726a396c6b5e2d9a35 Mon Sep 17 00:00:00 2001 From: bergquist Date: Thu, 5 Oct 2017 10:02:42 +0200 Subject: [PATCH 47/83] changelog: adds note about closing #9399 --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b40f20fde0f..dcad775f905 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,6 +27,7 @@ * **HTTP**: set net.Dialer.DualStack to true for all http clients [#9367](https://github.com/grafana/grafana/pull/9367) * **Alerting**: Add diff and percent diff as series reducers [#9386](https://github.com/grafana/grafana/pull/9386), thx [@shanhuhai5739](https://github.com/shanhuhai5739) * **Slack**: Allow images to be uploaded to slack when Token is precent [#7175](https://github.com/grafana/grafana/issues/7175), thx [@xginn8](https://github.com/xginn8) +* **Opsgenie**: Use their latest API instead of old version [#9399](https://github.com/grafana/grafana/pull/9399), thx [@cglrkn](https://github.com/cglrkn) ## Tech * **Go**: Grafana is now built using golang 1.9 From 273c17f3f3945d742335babffb0f0e0b14948f0b Mon Sep 17 00:00:00 2001 From: bergquist Date: Thu, 5 Oct 2017 10:43:06 +0200 Subject: [PATCH 48/83] changelog: add note about closing #9429 --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index dcad775f905..5601f866868 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,6 +28,7 @@ * **Alerting**: Add diff and percent diff as series reducers [#9386](https://github.com/grafana/grafana/pull/9386), thx [@shanhuhai5739](https://github.com/shanhuhai5739) * **Slack**: Allow images to be uploaded to slack when Token is precent [#7175](https://github.com/grafana/grafana/issues/7175), thx [@xginn8](https://github.com/xginn8) * **Opsgenie**: Use their latest API instead of old version [#9399](https://github.com/grafana/grafana/pull/9399), thx [@cglrkn](https://github.com/cglrkn) +* **Table**: Add support for displaying the timestamp with milliseconds [#9429](https://github.com/grafana/grafana/pull/9429), thx [@s1061123](https://github.com/s1061123) ## Tech * **Go**: Grafana is now built using golang 1.9 From 69b0e6350239d14cefca60a3d341a4baab1cb252 Mon Sep 17 00:00:00 2001 From: bergquist Date: Thu, 5 Oct 2017 11:16:31 +0200 Subject: [PATCH 49/83] changelog: adds note about closing #9226 --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5601f866868..a7648a3710a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,7 @@ * **Cloudwatch**: Add support for alerting using the cloudwatch datasource [#8050](https://github.com/grafana/grafana/pull/8050), thx [@mtanda](https://github.com/mtanda) * **Pagerduty**: Include triggering series in pagerduty notification [#8479](https://github.com/grafana/grafana/issues/8479), thx [@rickymoorhouse](https://github.com/rickymoorhouse) * **Timezone**: Time ranges like Today & Yesterday now work correctly when timezone setting is set to UTC [#8916](https://github.com/grafana/grafana/issues/8916), thx [@ctide](https://github.com/ctide) +* **Prometheus**: Align $__interval with the step parameters. [#9226](https://github.com/grafana/grafana/pull/9226), thx [@alin-amana](https://github.com/alin-amana) ## Minor * **SMTP**: Make it possible to set specific EHLO for smtp client. [#9319](https://github.com/grafana/grafana/issues/9319) From 15ece1da0402c9716d0609d592762ca6fd7de042 Mon Sep 17 00:00:00 2001 From: bergquist Date: Thu, 5 Oct 2017 09:01:56 +0200 Subject: [PATCH 50/83] changes go version to 1.9.1 --- appveyor.yml | 2 +- circle.yml | 2 +- docs/sources/project/building_from_source.md | 2 +- scripts/build/Dockerfile | 6 +++--- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/appveyor.yml b/appveyor.yml index d626f6bd93f..19de1d3a793 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.9 + GOVERSION: 1.9.1 install: - rmdir c:\go /s /q diff --git a/circle.yml b/circle.yml index 0d535a9c1c2..ba44172ddf3 100644 --- a/circle.yml +++ b/circle.yml @@ -9,7 +9,7 @@ machine: GOPATH: "/home/ubuntu/.go_workspace" ORG_PATH: "github.com/grafana" REPO_PATH: "${ORG_PATH}/grafana" - GODIST: "go1.9.linux-amd64.tar.gz" + GODIST: "go1.9.1.linux-amd64.tar.gz" post: - mkdir -p ~/download - mkdir -p ~/docker diff --git a/docs/sources/project/building_from_source.md b/docs/sources/project/building_from_source.md index c3f65db618d..f8dcc9a949c 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.9](https://golang.org/dl/) +- [Go 1.9.1](https://golang.org/dl/) - [NodeJS LTS](https://nodejs.org/download/) - [Git](https://git-scm.com/downloads) diff --git a/scripts/build/Dockerfile b/scripts/build/Dockerfile index da9a99706bb..58a3c65a63f 100644 --- a/scripts/build/Dockerfile +++ b/scripts/build/Dockerfile @@ -23,10 +23,10 @@ RUN curl --silent --location https://rpm.nodesource.com/setup_6.x | bash - && \ RUN wget https://dl.yarnpkg.com/rpm/yarn.repo -O /etc/yum.repos.d/yarn.repo && \ yum install -y yarn --nogpgcheck && \ - wget https://storage.googleapis.com/golang/go1.9.linux-amd64.tar.gz && \ - tar -C /usr/local -xzf go1.9.linux-amd64.tar.gz + wget https://storage.googleapis.com/golang/go1.9.1.linux-amd64.tar.gz && \ + tar -C /usr/local -xzf go1.9.1.linux-amd64.tar.gz -ENV GOLANG_VERSION 1.9 +ENV GOLANG_VERSION 1.9.1 ENV PATH /usr/local/go/bin:$PATH RUN mkdir -p /go/src /go/bin && chmod -R 777 /go From eeb2e2c6c94328bef54f9bd49a76ed0dc395145e Mon Sep 17 00:00:00 2001 From: bergquist Date: Thu, 5 Oct 2017 10:21:44 +0200 Subject: [PATCH 51/83] build: install go based on env variable --- scripts/build/Dockerfile | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/scripts/build/Dockerfile b/scripts/build/Dockerfile index 58a3c65a63f..89b0a1a46dd 100644 --- a/scripts/build/Dockerfile +++ b/scripts/build/Dockerfile @@ -21,12 +21,14 @@ 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.9.1 + RUN wget https://dl.yarnpkg.com/rpm/yarn.repo -O /etc/yum.repos.d/yarn.repo && \ yum install -y yarn --nogpgcheck && \ - wget https://storage.googleapis.com/golang/go1.9.1.linux-amd64.tar.gz && \ - tar -C /usr/local -xzf go1.9.1.linux-amd64.tar.gz + wget https://storage.googleapis.com/golang/go${GOLANG_VERSION}.linux-amd64.tar.gz && \ + tar -C /usr/local -xzf go${GOLANG_VERSION}.linux-amd64.tar.gz + -ENV GOLANG_VERSION 1.9.1 ENV PATH /usr/local/go/bin:$PATH RUN mkdir -p /go/src /go/bin && chmod -R 777 /go From 0841eb94a8fdb62710d2b59a4baf679bfbed806f Mon Sep 17 00:00:00 2001 From: Daniel Lee Date: Thu, 5 Oct 2017 11:37:00 +0200 Subject: [PATCH 52/83] build: add noUnusedLocals to tsc parameters --- tsconfig.json | 1 + 1 file changed, 1 insertion(+) diff --git a/tsconfig.json b/tsconfig.json index 53b7fe13c27..3864befd0fd 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -18,6 +18,7 @@ "noImplicitThis": false, "noImplicitUseStrict":false, "noImplicitAny": false, + "noUnusedLocals": true, "baseUrl": "public", "paths": { "app": ["app"] From 7a7e8c57d118daa3921406c8e835186173335445 Mon Sep 17 00:00:00 2001 From: Patrick O'Carroll Date: Thu, 5 Oct 2017 13:27:36 +0200 Subject: [PATCH 53/83] fixed layout for column options, changed dropdown for date format kept old code --- public/app/plugins/panel/table/column_options.html | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/public/app/plugins/panel/table/column_options.html b/public/app/plugins/panel/table/column_options.html index 53e3c477346..12a58c77058 100644 --- a/public/app/plugins/panel/table/column_options.html +++ b/public/app/plugins/panel/table/column_options.html @@ -35,13 +35,16 @@
      -
      +
      - + +
      + +
      @@ -54,7 +57,7 @@
      -
      +
      From 531586bebafe80f25c63a00d42f8bd2a9180ac2b Mon Sep 17 00:00:00 2001 From: Jon Skarpeteig Date: Thu, 5 Oct 2017 13:34:07 +0200 Subject: [PATCH 54/83] Add Norwegian Krone denominator for currency See http://www.xe.com/symbols.php for currency symbols reference --- public/app/core/utils/kbn.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/public/app/core/utils/kbn.js b/public/app/core/utils/kbn.js index 8947ce5a902..1782e76ca23 100644 --- a/public/app/core/utils/kbn.js +++ b/public/app/core/utils/kbn.js @@ -402,6 +402,7 @@ function($, _, moment) { kbn.valueFormats.currencyRUB = kbn.formatBuilders.currency('₽'); kbn.valueFormats.currencyUAH = kbn.formatBuilders.currency('₴'); kbn.valueFormats.currencyBRL = kbn.formatBuilders.currency('R$'); + kbn.valueFormats.currencyNOK = kbn.formatBuilders.currency('kr'); // Data (Binary) kbn.valueFormats.bits = kbn.formatBuilders.binarySIPrefix('b'); @@ -756,6 +757,7 @@ function($, _, moment) { {text: 'Rubles (₽)', value: 'currencyRUB'}, {text: 'Hryvnias (₴)', value: 'currencyUAH'}, {text: 'Real (R$)', value: 'currencyBRL'}, + {text: 'Norwegian Krone (kr)', value: 'currencyNOK'}, ] }, { From 7479c82687892ff1d853bb0d6e50498d0f132aa0 Mon Sep 17 00:00:00 2001 From: Jon Skarpeteig Date: Thu, 5 Oct 2017 13:44:23 +0200 Subject: [PATCH 55/83] Update kbn.js Adding the other kr currencies not to offend any maintainers --- public/app/core/utils/kbn.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/public/app/core/utils/kbn.js b/public/app/core/utils/kbn.js index 1782e76ca23..f7029f9bdd4 100644 --- a/public/app/core/utils/kbn.js +++ b/public/app/core/utils/kbn.js @@ -402,7 +402,10 @@ function($, _, moment) { kbn.valueFormats.currencyRUB = kbn.formatBuilders.currency('₽'); kbn.valueFormats.currencyUAH = kbn.formatBuilders.currency('₴'); kbn.valueFormats.currencyBRL = kbn.formatBuilders.currency('R$'); + kbn.valueFormats.currencyDKK = kbn.formatBuilders.currency('kr'); + kbn.valueFormats.currencyISK = kbn.formatBuilders.currency('kr'); kbn.valueFormats.currencyNOK = kbn.formatBuilders.currency('kr'); + kbn.valueFormats.currencySEK = kbn.formatBuilders.currency('kr'); // Data (Binary) kbn.valueFormats.bits = kbn.formatBuilders.binarySIPrefix('b'); @@ -757,7 +760,11 @@ function($, _, moment) { {text: 'Rubles (₽)', value: 'currencyRUB'}, {text: 'Hryvnias (₴)', value: 'currencyUAH'}, {text: 'Real (R$)', value: 'currencyBRL'}, + {text: 'Danish Krone (kr)', value: 'currencyDKK'}, + {text: 'Icelandic Krone (kr)', value: 'currencyISK'}, {text: 'Norwegian Krone (kr)', value: 'currencyNOK'}, + {text: 'Swedish Krone (kr)', value: 'currencySEK'}, + ] }, { From 789473fd34674a29ce224af7974e270bab651d84 Mon Sep 17 00:00:00 2001 From: Jon Skarpeteig Date: Thu, 5 Oct 2017 13:46:42 +0200 Subject: [PATCH 56/83] Lint fix --- public/app/core/utils/kbn.js | 1 - 1 file changed, 1 deletion(-) diff --git a/public/app/core/utils/kbn.js b/public/app/core/utils/kbn.js index f7029f9bdd4..1daaa2f8ba3 100644 --- a/public/app/core/utils/kbn.js +++ b/public/app/core/utils/kbn.js @@ -764,7 +764,6 @@ function($, _, moment) { {text: 'Icelandic Krone (kr)', value: 'currencyISK'}, {text: 'Norwegian Krone (kr)', value: 'currencyNOK'}, {text: 'Swedish Krone (kr)', value: 'currencySEK'}, - ] }, { From 79feba000414a7a5f2946c64a0e137877761effa Mon Sep 17 00:00:00 2001 From: Alin Sinpalean Date: Thu, 5 Oct 2017 13:51:02 +0200 Subject: [PATCH 57/83] Fix spelling on 404 page. --- public/app/partials/error.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/public/app/partials/error.html b/public/app/partials/error.html index b1ae05e7e93..2f75581fca5 100644 --- a/public/app/partials/error.html +++ b/public/app/partials/error.html @@ -37,13 +37,13 @@
      -

      Chance your are one the page your'e looking for.

      +

      Chances you are on the page you are looking for.

      0%

      Sorry for the inconvenience

      Please go back to your home dashboard and try again.

      -

      If the error persists, seek help att the community site.

      +

      If the error persists, seek help on the community site.

      From 1590729a242f817d80058b2964c8b76433e9295d Mon Sep 17 00:00:00 2001 From: bergquist Date: Thu, 5 Oct 2017 15:00:12 +0200 Subject: [PATCH 58/83] changelog: adds note about closing #9208 --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a7648a3710a..928596a35ef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,7 @@ * **Pagerduty**: Include triggering series in pagerduty notification [#8479](https://github.com/grafana/grafana/issues/8479), thx [@rickymoorhouse](https://github.com/rickymoorhouse) * **Timezone**: Time ranges like Today & Yesterday now work correctly when timezone setting is set to UTC [#8916](https://github.com/grafana/grafana/issues/8916), thx [@ctide](https://github.com/ctide) * **Prometheus**: Align $__interval with the step parameters. [#9226](https://github.com/grafana/grafana/pull/9226), thx [@alin-amana](https://github.com/alin-amana) +* **Prometheus**: Autocomplete for label name and label value [#9208](https://github.com/grafana/grafana/pull/9208), thx [@mtanda](https://github.com/mtanda) ## Minor * **SMTP**: Make it possible to set specific EHLO for smtp client. [#9319](https://github.com/grafana/grafana/issues/9319) From 6a030b2e8c067cae5b4e77b22fec864e9286f56f Mon Sep 17 00:00:00 2001 From: Joseph Weigl Date: Thu, 5 Oct 2017 16:00:55 +0200 Subject: [PATCH 59/83] Change empty string checks and improve logging --- pkg/services/alerting/notifiers/hipchat.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pkg/services/alerting/notifiers/hipchat.go b/pkg/services/alerting/notifiers/hipchat.go index c131b80c578..f1f63d42a04 100644 --- a/pkg/services/alerting/notifiers/hipchat.go +++ b/pkg/services/alerting/notifiers/hipchat.go @@ -115,7 +115,7 @@ func (this *HipChatNotifier) Notify(evalContext *alerting.EvalContext) error { message += " " + evalContext.Rule.Message } - if len(message) < 1 { + if message == "" { message = evalContext.GetNotificationTitle() + " in state " + evalContext.GetStateModel().Text } @@ -143,7 +143,7 @@ func (this *HipChatNotifier) Notify(evalContext *alerting.EvalContext) error { "date": evalContext.EndTime.Unix(), "attributes": attributes, } - if len(evalContext.ImagePublicUrl) > 0 { + if evalContext.ImagePublicUrl != "" { card["thumbnail"] = map[string]interface{}{ "url": evalContext.ImagePublicUrl, "url@2x": evalContext.ImagePublicUrl, @@ -162,7 +162,7 @@ func (this *HipChatNotifier) Notify(evalContext *alerting.EvalContext) error { hipUrl := fmt.Sprintf("%s/v2/room/%s/notification?auth_token=%s", this.Url, this.RoomId, this.ApiKey) data, _ := json.Marshal(&body) - this.log.Debug(string(data)) + this.log.Info("Request payload", "json", string(data)) cmd := &models.SendWebhookSync{Url: hipUrl, Body: string(data)} if err := bus.DispatchCtx(evalContext.Ctx, cmd); err != nil { From eb4a568240790bdda1be5cf9e4b562772390d5cc Mon Sep 17 00:00:00 2001 From: shabscan Date: Thu, 5 Oct 2017 10:11:57 -0400 Subject: [PATCH 60/83] Fixed typo --- ROADMAP.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ROADMAP.md b/ROADMAP.md index 62e6719c4f0..3ce0c33f088 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -17,7 +17,7 @@ But it will give you an idea of our current vision and plan. ### Long term - Backend plugins to support more Auth options, Alerting data sources & notifications -- Universial time series transformations for any data source (meta queries) +- Universal time series transformations for any data source (meta queries) - Reporting - Web socket & live data streams - Migrate to Angular2 or react From 4cda9d01281ffd43ac42e3c45c8deaf52dc1aa18 Mon Sep 17 00:00:00 2001 From: bergquist Date: Thu, 5 Oct 2017 16:12:53 +0200 Subject: [PATCH 61/83] changelog: adds note about closing #9110 --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 928596a35ef..938c46dd13b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -31,6 +31,7 @@ * **Slack**: Allow images to be uploaded to slack when Token is precent [#7175](https://github.com/grafana/grafana/issues/7175), thx [@xginn8](https://github.com/xginn8) * **Opsgenie**: Use their latest API instead of old version [#9399](https://github.com/grafana/grafana/pull/9399), thx [@cglrkn](https://github.com/cglrkn) * **Table**: Add support for displaying the timestamp with milliseconds [#9429](https://github.com/grafana/grafana/pull/9429), thx [@s1061123](https://github.com/s1061123) +* **Hipchat**: Add metrics, message and image to hipchat notifications [#9110](https://github.com/grafana/grafana/issues/9110), thx [@eloo](https://github.com/eloo) ## Tech * **Go**: Grafana is now built using golang 1.9 From bb52f00d2d6d6135f23dade7573250206fe48311 Mon Sep 17 00:00:00 2001 From: Patrick O'Carroll Date: Thu, 5 Oct 2017 16:29:01 +0200 Subject: [PATCH 62/83] removed commented line --- public/app/plugins/panel/table/column_options.html | 1 - 1 file changed, 1 deletion(-) diff --git a/public/app/plugins/panel/table/column_options.html b/public/app/plugins/panel/table/column_options.html index 12a58c77058..b729a289970 100644 --- a/public/app/plugins/panel/table/column_options.html +++ b/public/app/plugins/panel/table/column_options.html @@ -41,7 +41,6 @@
      -
      From 22cfb614bf8fc14fb85eb6891e3f96342c51932d Mon Sep 17 00:00:00 2001 From: Geno Date: Thu, 5 Oct 2017 17:34:13 +0200 Subject: [PATCH 63/83] fix type in readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 44bf4398fc2..40ec43e1c16 100644 --- a/README.md +++ b/README.md @@ -45,7 +45,7 @@ yarn install --pure-lockfile npm run build ``` -To rebuild frontend assets (typesript, sass etc) as you change them start the watcher via. +To rebuild frontend assets (typescript, sass etc) as you change them start the watcher via. ```bash npm run watch From 19b5d91b7574d36157f779ab6def22ad279beceb Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Thu, 5 Oct 2017 19:34:12 +0300 Subject: [PATCH 64/83] Series color picker fix (#9442) * colorpicker: fix spectrum color update on series color picker * colorpicker: minor refactor --- public/app/core/components/colorpicker/ColorPickerPopover.tsx | 2 +- public/app/core/components/colorpicker/SeriesColorPicker.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/public/app/core/components/colorpicker/ColorPickerPopover.tsx b/public/app/core/components/colorpicker/ColorPickerPopover.tsx index 49e1b1e2105..5c1d90d6046 100644 --- a/public/app/core/components/colorpicker/ColorPickerPopover.tsx +++ b/public/app/core/components/colorpicker/ColorPickerPopover.tsx @@ -88,7 +88,7 @@ export class ColorPickerPopover extends React.Component { ); const spectrumTab = (
      - +
      ); const currentTab = this.state.tab === 'palette' ? paletteTab : spectrumTab; diff --git a/public/app/core/components/colorpicker/SeriesColorPicker.tsx b/public/app/core/components/colorpicker/SeriesColorPicker.tsx index e7294aa6281..2ee2d7571b3 100644 --- a/public/app/core/components/colorpicker/SeriesColorPicker.tsx +++ b/public/app/core/components/colorpicker/SeriesColorPicker.tsx @@ -44,7 +44,7 @@ export class SeriesColorPicker extends React.Component { return (
      {this.props.series && this.renderAxisSelection()} - +
      ); } From 81be4e261242a24b50d32c42f415c23c52404381 Mon Sep 17 00:00:00 2001 From: Patrick O'Carroll Date: Thu, 5 Oct 2017 19:01:03 +0200 Subject: [PATCH 65/83] Update codebox (#9430) * updated the codeboxes in docs * codebox change from json to http --- docs/sources/administration/cli.md | 2 +- .../features/datasources/cloudwatch.md | 19 +- .../features/datasources/elasticsearch.md | 6 +- docs/sources/http_api/admin.md | 438 ++++++++++-------- docs/sources/http_api/alerting.md | 282 +++++------ docs/sources/http_api/auth.md | 95 ++-- docs/sources/http_api/dashboard.md | 158 ++++--- docs/sources/http_api/data_source.md | 329 +++++++------ docs/sources/http_api/org.md | 424 +++++++++-------- docs/sources/http_api/other.md | 86 ++-- docs/sources/http_api/preferences.md | 92 ++-- docs/sources/http_api/snapshot.md | 114 +++-- docs/sources/http_api/user.md | 399 +++++++++------- docs/sources/installation/behind_proxy.md | 10 +- docs/sources/installation/configuration.md | 181 +++++--- docs/sources/installation/debian.md | 8 +- docs/sources/installation/docker.md | 14 +- docs/sources/installation/ldap.md | 4 +- docs/sources/installation/mac.md | 12 +- docs/sources/installation/rpm.md | 67 ++- docs/sources/installation/upgrading.md | 10 +- .../plugins/developing/code-styleguide.md | 4 +- .../sources/plugins/developing/datasources.md | 2 +- docs/sources/plugins/installation.md | 20 +- docs/sources/project/building_from_source.md | 16 +- docs/sources/reference/dashboard.md | 2 +- docs/sources/reference/sharing.md | 2 +- docs/sources/tutorials/api_org_token_howto.md | 12 +- docs/sources/tutorials/hubot_howto.md | 26 +- 29 files changed, 1574 insertions(+), 1260 deletions(-) diff --git a/docs/sources/administration/cli.md b/docs/sources/administration/cli.md index 645f75ab412..2be1881cfab 100644 --- a/docs/sources/administration/cli.md +++ b/docs/sources/administration/cli.md @@ -41,7 +41,7 @@ then there are two flags that can be used to set homepath and the config file pa If you have not lost the admin password then it is better to set in the Grafana UI. If you need to set the password in a script then the [Grafana API](http://docs.grafana.org/http_api/user/#change-password) can be used. Here is an example with curl using basic auth: -``` +```bash curl -X PUT -H "Content-Type: application/json" -d '{ "oldPassword": "admin", "newPassword": "newpass", diff --git a/docs/sources/features/datasources/cloudwatch.md b/docs/sources/features/datasources/cloudwatch.md index 61dd90d2881..29da06bfb1e 100644 --- a/docs/sources/features/datasources/cloudwatch.md +++ b/docs/sources/features/datasources/cloudwatch.md @@ -50,11 +50,12 @@ Create a file at `~/.aws/credentials`. That is the `HOME` path for user running Example content: - [default] - aws_access_key_id = asdsadasdasdasd - aws_secret_access_key = dasdasdsadasdasdasdsa - region = us-west-2 - +```bash +[default] +aws_access_key_id = asdsadasdasdasd +aws_secret_access_key = dasdasdsadasdasdasdsa +region = us-west-2 +``` ## Metric Query Editor @@ -117,7 +118,9 @@ Filters syntax: Example `ec2_instance_attribute()` query - ec2_instance_attribute(us-east-1, InstanceId, { "tag:Environment": [ "production" ] }) +```javascript +ec2_instance_attribute(us-east-1, InstanceId, { "tag:Environment": [ "production" ] }) +``` ### Selecting Attributes @@ -156,7 +159,9 @@ Tags can be selected by prepending the tag name with `Tags.` Example `ec2_instance_attribute()` query - ec2_instance_attribute(us-east-1, Tags.Name, { "tag:Team": [ "sysops" ] }) +```javascript +ec2_instance_attribute(us-east-1, Tags.Name, { "tag:Team": [ "sysops" ] }) +``` ## Cost diff --git a/docs/sources/features/datasources/elasticsearch.md b/docs/sources/features/datasources/elasticsearch.md index 25cdb98c8c5..be4e3e78e49 100644 --- a/docs/sources/features/datasources/elasticsearch.md +++ b/docs/sources/features/datasources/elasticsearch.md @@ -38,8 +38,10 @@ Proxy access means that the Grafana backend will proxy all requests from the bro If you select direct access you must update your Elasticsearch configuration to allow other domains to access Elasticsearch from the browser. You do this by specifying these to options in your **elasticsearch.yml** config file. - http.cors.enabled: true - http.cors.allow-origin: "*" +```bash +http.cors.enabled: true +http.cors.allow-origin: "*" +``` ### Index settings diff --git a/docs/sources/http_api/admin.md b/docs/sources/http_api/admin.md index 55bec79c7f8..3ef5fb1136a 100644 --- a/docs/sources/http_api/admin.md +++ b/docs/sources/http_api/admin.md @@ -23,158 +23,162 @@ Only works with Basic Authentication (username and password). See [introduction] **Example Request**: - GET /api/admin/settings - Accept: application/json - Content-Type: application/json +```bash +GET /api/admin/settings +Accept: application/json +Content-Type: application/json +``` **Example Response**: - HTTP/1.1 200 - Content-Type: application/json - - { - "DEFAULT": - { - "app_mode":"production"}, - "analytics": - { - "google_analytics_ua_id":"", - "reporting_enabled":"false" - }, - "auth.anonymous":{ - "enabled":"true", - "org_name":"Main Org.", - "org_role":"Viewer" - }, - "auth.basic":{ - "enabled":"false" - }, - "auth.github":{ - "allow_sign_up":"false", - "allowed_domains":"", - "allowed_organizations":"", - "api_url":"https://api.github.com/user", - "auth_url":"https://github.com/login/oauth/authorize", - "client_id":"some_id", - "client_secret":"************", - "enabled":"false", - "scopes":"user:email", - "team_ids":"", - "token_url":"https://github.com/login/oauth/access_token" - }, - "auth.google":{ - "allow_sign_up":"false","allowed_domains":"", - "api_url":"https://www.googleapis.com/oauth2/v1/userinfo", - "auth_url":"https://accounts.google.com/o/oauth2/auth", - "client_id":"some_client_id", - "client_secret":"************", - "enabled":"false", - "scopes":"https://www.googleapis.com/auth/userinfo.profile https://www.googleapis.com/auth/userinfo.email", - "token_url":"https://accounts.google.com/o/oauth2/token" - }, - "auth.ldap":{ - "config_file":"/etc/grafana/ldap.toml", - "enabled":"false" - }, - "auth.proxy":{ - "auto_sign_up":"true", - "enabled":"false", - "header_name":"X-WEBAUTH-USER", - "header_property":"username" - }, - "dashboards.json":{ - "enabled":"false", - "path":"/var/lib/grafana/dashboards" - }, - "database":{ - "host":"127.0.0.1:0000", - "name":"grafana", - "password":"************", - "path":"grafana.db", - "ssl_mode":"disable", - "type":"sqlite3", - "user":"root" - }, - "emails":{ - "templates_pattern":"emails/*.html", - "welcome_email_on_sign_up":"false" - }, - "event_publisher":{ - "enabled":"false", - "exchange":"grafana_events", - "rabbitmq_url":"amqp://localhost/" - }, - "log":{ - "buffer_len":"10000", - "level":"Info", - "mode":"file" - }, - "log.console":{ - "level":"" - }, - "log.file":{ - "daily_rotate":"true", - "file_name":"", - "level":"", - "log_rotate":"true", - "max_days":"7", - "max_lines":"1000000", - "max_lines_shift":"28", - "max_size_shift":"" - }, - "paths":{ - "data":"/tsdb/grafana", - "logs":"/logs/apps/grafana"}, - "security":{ - "admin_password":"************", - "admin_user":"admin", - "cookie_remember_name":"grafana_remember", - "cookie_username":"grafana_user", - "disable_gravatar":"false", - "login_remember_days":"7", - "secret_key":"************" - }, - "server":{ - "cert_file":"", - "cert_key":"", - "domain":"mygraf.com", - "enable_gzip":"false", - "enforce_domain":"false", - "http_addr":"127.0.0.1", - "http_port":"0000", - "protocol":"http", - "root_url":"%(protocol)s://%(domain)s:%(http_port)s/", - "router_logging":"true", - "data_proxy_logging":"true", - "static_root_path":"public" - }, - "session":{ - "cookie_name":"grafana_sess", - "cookie_secure":"false", - "gc_interval_time":"", - "provider":"file", - "provider_config":"sessions", - "session_life_time":"86400" - }, - "smtp":{ - "cert_file":"", - "enabled":"false", - "from_address":"admin@grafana.localhost", - "from_name":"Grafana", - "ehlo_identity":"dashboard.example.com", - "host":"localhost:25", - "key_file":"", - "password":"************", - "skip_verify":"false", - "user":""}, - "users":{ - "allow_org_create":"true", - "allow_sign_up":"false", - "auto_assign_org":"true", - "auto_assign_org_role":"Viewer" - } - } +```bash +HTTP/1.1 200 +Content-Type: application/json +{ +"DEFAULT": +{ + "app_mode":"production"}, + "analytics": + { + "google_analytics_ua_id":"", + "reporting_enabled":"false" + }, + "auth.anonymous":{ + "enabled":"true", + "org_name":"Main Org.", + "org_role":"Viewer" + }, + "auth.basic":{ + "enabled":"false" + }, + "auth.github":{ + "allow_sign_up":"false", + "allowed_domains":"", + "allowed_organizations":"", + "api_url":"https://api.github.com/user", + "auth_url":"https://github.com/login/oauth/authorize", + "client_id":"some_id", + "client_secret":"************", + "enabled":"false", + "scopes":"user:email", + "team_ids":"", + "token_url":"https://github.com/login/oauth/access_token" + }, + "auth.google":{ + "allow_sign_up":"false","allowed_domains":"", + "api_url":"https://www.googleapis.com/oauth2/v1/userinfo", + "auth_url":"https://accounts.google.com/o/oauth2/auth", + "client_id":"some_client_id", + "client_secret":"************", + "enabled":"false", + "scopes":"https://www.googleapis.com/auth/userinfo.profile https://www.googleapis.com/auth/userinfo.email", + "token_url":"https://accounts.google.com/o/oauth2/token" + }, + "auth.ldap":{ + "config_file":"/etc/grafana/ldap.toml", + "enabled":"false" + }, + "auth.proxy":{ + "auto_sign_up":"true", + "enabled":"false", + "header_name":"X-WEBAUTH-USER", + "header_property":"username" + }, + "dashboards.json":{ + "enabled":"false", + "path":"/var/lib/grafana/dashboards" + }, + "database":{ + "host":"127.0.0.1:0000", + "name":"grafana", + "password":"************", + "path":"grafana.db", + "ssl_mode":"disable", + "type":"sqlite3", + "user":"root" + }, + "emails":{ + "templates_pattern":"emails/*.html", + "welcome_email_on_sign_up":"false" + }, + "event_publisher":{ + "enabled":"false", + "exchange":"grafana_events", + "rabbitmq_url":"amqp://localhost/" + }, + "log":{ + "buffer_len":"10000", + "level":"Info", + "mode":"file" + }, + "log.console":{ + "level":"" + }, + "log.file":{ + "daily_rotate":"true", + "file_name":"", + "level":"", + "log_rotate":"true", + "max_days":"7", + "max_lines":"1000000", + "max_lines_shift":"28", + "max_size_shift":"" + }, + "paths":{ + "data":"/tsdb/grafana", + "logs":"/logs/apps/grafana"}, + "security":{ + "admin_password":"************", + "admin_user":"admin", + "cookie_remember_name":"grafana_remember", + "cookie_username":"grafana_user", + "disable_gravatar":"false", + "login_remember_days":"7", + "secret_key":"************" + }, + "server":{ + "cert_file":"", + "cert_key":"", + "domain":"mygraf.com", + "enable_gzip":"false", + "enforce_domain":"false", + "http_addr":"127.0.0.1", + "http_port":"0000", + "protocol":"http", + "root_url":"%(protocol)s://%(domain)s:%(http_port)s/", + "router_logging":"true", + "data_proxy_logging":"true", + "static_root_path":"public" + }, + "session":{ + "cookie_name":"grafana_sess", + "cookie_secure":"false", + "gc_interval_time":"", + "provider":"file", + "provider_config":"sessions", + "session_life_time":"86400" + }, + "smtp":{ + "cert_file":"", + "enabled":"false", + "from_address":"admin@grafana.localhost", + "from_name":"Grafana", + "ehlo_identity":"dashboard.example.com", + "host":"localhost:25", + "key_file":"", + "password":"************", + "skip_verify":"false", + "user":"" + }, + "users":{ + "allow_org_create":"true", + "allow_sign_up":"false", + "auto_assign_org":"true", + "auto_assign_org_role":"Viewer" + } +} +``` ## Grafana Stats `GET /api/admin/stats` @@ -183,26 +187,30 @@ Only works with Basic Authentication (username and password). See [introduction] **Example Request**: - GET /api/admin/stats - Accept: application/json - Content-Type: application/json +```bash +GET /api/admin/stats +Accept: application/json +Content-Type: application/json +``` **Example Response**: - HTTP/1.1 200 - Content-Type: application/json +```json +HTTP/1.1 200 +Content-Type: application/json - { - "user_count":2, - "org_count":1, - "dashboard_count":4, - "db_snapshot_count":2, - "db_tag_count":6, - "data_source_count":1, - "playlist_count":1, - "starred_db_count":2, - "grafana_admin_count":2 - } +{ + "user_count":2, + "org_count":1, + "dashboard_count":4, + "db_snapshot_count":2, + "db_tag_count":6, + "data_source_count":1, + "playlist_count":1, + "starred_db_count":2, + "grafana_admin_count":2 +} +``` ## Global Users @@ -211,24 +219,28 @@ Only works with Basic Authentication (username and password). See [introduction] Create new user. Only works with Basic Authentication (username and password). See [introduction](http://docs.grafana.org/http_api/admin/#admin-api) for an explanation. **Example Request**: +```json - POST /api/admin/users HTTP/1.1 - Accept: application/json - Content-Type: application/json +POST /api/admin/users HTTP/1.1 +Accept: application/json +Content-Type: application/json - { - "name":"User", - "email":"user@graf.com", - "login":"user", - "password":"userpassword" - } +{ + "name":"User", + "email":"user@graf.com", + "login":"user", + "password":"userpassword" +} +``` **Example Response**: - HTTP/1.1 200 - Content-Type: application/json +```json +HTTP/1.1 200 +Content-Type: application/json - {"id":5,"message":"User created"} +{"id":5,"message":"User created"} +``` ## Password for User @@ -239,18 +251,22 @@ Change password for a specific user. **Example Request**: - PUT /api/admin/users/2/password HTTP/1.1 - Accept: application/json - Content-Type: application/json +```json +PUT /api/admin/users/2/password HTTP/1.1 +Accept: application/json +Content-Type: application/json - {"password":"userpassword"} +{"password":"userpassword"} +``` **Example Response**: - HTTP/1.1 200 - Content-Type: application/json +```json +HTTP/1.1 200 +Content-Type: application/json - {"message": "User password updated"} +{"message": "User password updated"} +``` ## Permissions @@ -260,18 +276,22 @@ Only works with Basic Authentication (username and password). See [introduction] **Example Request**: - PUT /api/admin/users/2/permissions HTTP/1.1 - Accept: application/json - Content-Type: application/json +```json +PUT /api/admin/users/2/permissions HTTP/1.1 +Accept: application/json +Content-Type: application/json - {"isGrafanaAdmin": true} +{"isGrafanaAdmin": true} +``` **Example Response**: - HTTP/1.1 200 - Content-Type: application/json +```json +HTTP/1.1 200 +Content-Type: application/json - {message: "User permissions updated"} +{message: "User permissions updated"} +``` ## Delete global User @@ -281,16 +301,20 @@ Only works with Basic Authentication (username and password). See [introduction] **Example Request**: - DELETE /api/admin/users/2 HTTP/1.1 - Accept: application/json - Content-Type: application/json +```json +DELETE /api/admin/users/2 HTTP/1.1 +Accept: application/json +Content-Type: application/json +``` **Example Response**: - HTTP/1.1 200 - Content-Type: application/json +```json +HTTP/1.1 200 +Content-Type: application/json - {message: "User deleted"} +{message: "User deleted"} +``` ## Pause all alerts @@ -300,13 +324,15 @@ Only works with Basic Authentication (username and password). See [introduction] **Example Request**: - POST /api/admin/pause-all-alerts HTTP/1.1 - Accept: application/json - Content-Type: application/json +```json +POST /api/admin/pause-all-alerts HTTP/1.1 +Accept: application/json +Content-Type: application/json - { - "paused": true - } +{ + "paused": true +} +``` JSON Body schema: @@ -314,7 +340,9 @@ JSON Body schema: **Example Response**: - HTTP/1.1 200 - Content-Type: application/json +```json +HTTP/1.1 200 +Content-Type: application/json - {state: "new state", message: "alerts pause/un paused", "alertsAffected": 100} +{state: "new state", message: "alerts pause/un paused", "alertsAffected": 100} +``` \ No newline at end of file diff --git a/docs/sources/http_api/alerting.md b/docs/sources/http_api/alerting.md index 1aab7253373..c5172c64203 100644 --- a/docs/sources/http_api/alerting.md +++ b/docs/sources/http_api/alerting.md @@ -23,11 +23,12 @@ This API can also be used to create, update and delete alert notifications. **Example Request**: - GET /api/alerts HTTP/1.1 - Accept: application/json - Content-Type: application/json - Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk - +```http +GET /api/alerts HTTP/1.1 +Accept: application/json +Content-Type: application/json +Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk +``` Querystring Parameters: These parameters are used as querystring parameters. For example: @@ -41,28 +42,30 @@ This API can also be used to create, update and delete alert notifications. **Example Response**: - HTTP/1.1 200 - Content-Type: application/json - [ +```http +HTTP/1.1 200 +Content-Type: application/json +[ + { + "id": 1, + "dashboardId": 1, + "panelId": 1, + "name": "fire place sensor", + "message": "Someone is trying to break in through the fire place", + "state": "alerting", + "evalDate": "0001-01-01T00:00:00Z", + "evalData": [ { - "id": 1, - "dashboardId": 1, - "panelId": 1, - "name": "fire place sensor", - "message": "Someone is trying to break in through the fire place", - "state": "alerting", - "evalDate": "0001-01-01T00:00:00Z", - "evalData": [ - { - "metric": "fire", - "tags": null, - "value": 5.349999999999999 - } - "newStateDate": "2016-12-25", - "executionError": "", - "dashboardUri": "http://grafana.com/dashboard/db/sensors" + "metric": "fire", + "tags": null, + "value": 5.349999999999999 } - ] + "newStateDate": "2016-12-25", + "executionError": "", + "dashboardUri": "http://grafana.com/dashboard/db/sensors" + } +] +``` ## Get one alert @@ -70,26 +73,30 @@ This API can also be used to create, update and delete alert notifications. **Example Request**: - GET /api/alerts/1 HTTP/1.1 - Accept: application/json - Content-Type: application/json - Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk +```http +GET /api/alerts/1 HTTP/1.1 +Accept: application/json +Content-Type: application/json +Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk +``` **Example Response**: - HTTP/1.1 200 - Content-Type: application/json - { - "id": 1, - "dashboardId": 1, - "panelId": 1, - "name": "fire place sensor", - "message": "Someone is trying to break in through the fire place", - "state": "alerting", - "newStateDate": "2016-12-25", - "executionError": "", - "dashboardUri": "http://grafana.com/dashboard/db/sensors" - } +```http +HTTP/1.1 200 +Content-Type: application/json +{ + "id": 1, + "dashboardId": 1, + "panelId": 1, + "name": "fire place sensor", + "message": "Someone is trying to break in through the fire place", + "state": "alerting", + "newStateDate": "2016-12-25", + "executionError": "", + "dashboardUri": "http://grafana.com/dashboard/db/sensors" +} +``` ## Pause alert @@ -97,14 +104,16 @@ This API can also be used to create, update and delete alert notifications. **Example Request**: - POST /api/alerts/1/pause HTTP/1.1 - Accept: application/json - Content-Type: application/json - Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk +```http +POST /api/alerts/1/pause HTTP/1.1 +Accept: application/json +Content-Type: application/json +Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk - { - "paused": true - } +{ + "paused": true +} +``` The :id query parameter is the id of the alert to be paused or unpaused. @@ -114,13 +123,15 @@ JSON Body Schema: **Example Response**: - HTTP/1.1 200 - Content-Type: application/json - { - "alertId": 1, - "state": "Paused", - "message": "alert paused" - } +```http +HTTP/1.1 200 +Content-Type: application/json +{ + "alertId": 1, + "state": "Paused", + "message": "alert paused" +} +``` ## Get alert notifications @@ -128,26 +139,29 @@ JSON Body Schema: **Example Request**: - GET /api/alert-notifications HTTP/1.1 - Accept: application/json - Content-Type: application/json - Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk - +```http +GET /api/alert-notifications HTTP/1.1 +Accept: application/json +Content-Type: application/json +Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk +``` **Example Response**: - HTTP/1.1 200 - Content-Type: application/json +```http +HTTP/1.1 200 +Content-Type: application/json - { - "id": 1, - "name": "Team A", - "type": "email", - "isDefault": true, - "created": "2017-01-01 12:45", - "updated": "2017-01-01 12:45" - } +{ + "id": 1, + "name": "Team A", + "type": "email", + "isDefault": true, + "created": "2017-01-01 12:45", + "updated": "2017-01-01 12:45" +} +``` ## Create alert notification @@ -155,34 +169,37 @@ JSON Body Schema: **Example Request**: - POST /api/alert-notifications HTTP/1.1 - Accept: application/json - Content-Type: application/json - Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk - - { - "name": "new alert notification", //Required - "type": "email", //Required - "isDefault": false, - "settings": { - "addresses": "carl@grafana.com;dev@grafana.com" - } - } +```http +POST /api/alert-notifications HTTP/1.1 +Accept: application/json +Content-Type: application/json +Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk +{ + "name": "new alert notification", //Required + "type": "email", //Required + "isDefault": false, + "settings": { + "addresses": "carl@grafana.com;dev@grafana.com" + } +} +``` **Example Response**: - HTTP/1.1 200 - Content-Type: application/json - { - "id": 1, - "name": "new alert notification", - "type": "email", - "isDefault": false, - "settings": { addresses: "carl@grafana.com;dev@grafana.com"} } - "created": "2017-01-01 12:34", - "updated": "2017-01-01 12:34" - } +```http +HTTP/1.1 200 +Content-Type: application/json +{ + "id": 1, + "name": "new alert notification", + "type": "email", + "isDefault": false, + "settings": { addresses: "carl@grafana.com;dev@grafana.com"} } + "created": "2017-01-01 12:34", + "updated": "2017-01-01 12:34" +} +``` ## Update alert notification @@ -190,35 +207,38 @@ JSON Body Schema: **Example Request**: - PUT /api/alert-notifications/1 HTTP/1.1 - Accept: application/json - Content-Type: application/json - Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk - - { - "id": 1, - "name": "new alert notification", //Required - "type": "email", //Required - "isDefault": false, - "settings": { - "addresses: "carl@grafana.com;dev@grafana.com" - } - } +```http +PUT /api/alert-notifications/1 HTTP/1.1 +Accept: application/json +Content-Type: application/json +Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk +{ + "id": 1, + "name": "new alert notification", //Required + "type": "email", //Required + "isDefault": false, + "settings": { + "addresses: "carl@grafana.com;dev@grafana.com" + } +} +``` **Example Response**: - HTTP/1.1 200 - Content-Type: application/json - { - "id": 1, - "name": "new alert notification", - "type": "email", - "isDefault": false, - "settings": { addresses: "carl@grafana.com;dev@grafana.com"} } - "created": "2017-01-01 12:34", - "updated": "2017-01-01 12:34" - } +```http +HTTP/1.1 200 +Content-Type: application/json +{ + "id": 1, + "name": "new alert notification", + "type": "email", + "isDefault": false, + "settings": { addresses: "carl@grafana.com;dev@grafana.com"} } + "created": "2017-01-01 12:34", + "updated": "2017-01-01 12:34" +} +``` ## Delete alert notification @@ -226,15 +246,19 @@ JSON Body Schema: **Example Request**: - DELETE /api/alert-notifications/1 HTTP/1.1 - Accept: application/json - Content-Type: application/json - Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk +```http +DELETE /api/alert-notifications/1 HTTP/1.1 +Accept: application/json +Content-Type: application/json +Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk +``` **Example Response**: - HTTP/1.1 200 - Content-Type: application/json - { - "message": "Notification deleted" - } +```http +HTTP/1.1 200 +Content-Type: application/json +{ + "message": "Notification deleted" +} +``` \ No newline at end of file diff --git a/docs/sources/http_api/auth.md b/docs/sources/http_api/auth.md index d8ded124ac5..b526031fdeb 100644 --- a/docs/sources/http_api/auth.md +++ b/docs/sources/http_api/auth.md @@ -21,7 +21,7 @@ If basic auth is enabled (it is enabled by default) you can authenticate your HT standard basic auth. Basic auth will also authenticate LDAP users. curl example: -``` +```bash ?curl http://admin:admin@localhost:3000/api/org {"id":1,"name":"Main Org."} ``` @@ -36,9 +36,11 @@ You use the token in all requests in the `Authorization` header, like this: **Example**: - GET http://your.grafana.com/api/dashboards/db/mydash HTTP/1.1 - Accept: application/json - Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk +```http +GET http://your.grafana.com/api/dashboards/db/mydash HTTP/1.1 +Accept: application/json +Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk +``` The `Authorization` header value should be `Bearer `. @@ -50,28 +52,32 @@ The `Authorization` header value should be `Bearer `. **Example Request**: - GET /api/auth/keys HTTP/1.1 - Accept: application/json - Content-Type: application/json - Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk +```http +GET /api/auth/keys HTTP/1.1 +Accept: application/json +Content-Type: application/json +Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk +``` **Example Response**: - HTTP/1.1 200 - Content-Type: application/json +```http +HTTP/1.1 200 +Content-Type: application/json - [ - { - "id": 3, - "name": "API", - "role": "Admin" - }, - { - "id": 1, - "name": "TestAdmin", - "role": "Admin" - } - ] +[ + { + "id": 3, + "name": "API", + "role": "Admin" + }, + { + "id": 1, + "name": "TestAdmin", + "role": "Admin" + } +] +``` ## Create API Key @@ -79,15 +85,17 @@ The `Authorization` header value should be `Bearer `. **Example Request**: - POST /api/auth/keys HTTP/1.1 - Accept: application/json - Content-Type: application/json - Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk +```http +POST /api/auth/keys HTTP/1.1 +Accept: application/json +Content-Type: application/json +Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk - { - "name": "mykey", - "role": "Admin" - } +{ + "name": "mykey", + "role": "Admin" +} +``` JSON Body schema: @@ -96,10 +104,12 @@ JSON Body schema: **Example Response**: - HTTP/1.1 200 - Content-Type: application/json +```http +HTTP/1.1 200 +Content-Type: application/json - {"name":"mykey","key":"eyJrIjoiWHZiSWd3NzdCYUZnNUtibE9obUpESmE3bzJYNDRIc0UiLCJuIjoibXlrZXkiLCJpZCI6MX1="} +{"name":"mykey","key":"eyJrIjoiWHZiSWd3NzdCYUZnNUtibE9obUpESmE3bzJYNDRIc0UiLCJuIjoibXlrZXkiLCJpZCI6MX1="} +``` ## Delete API Key @@ -107,14 +117,17 @@ JSON Body schema: **Example Request**: - DELETE /api/auth/keys/3 HTTP/1.1 - Accept: application/json - Content-Type: application/json - Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk - +```http +DELETE /api/auth/keys/3 HTTP/1.1 +Accept: application/json +Content-Type: application/json +Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk +``` **Example Response**: - HTTP/1.1 200 - Content-Type: application/json +```http +HTTP/1.1 200 +Content-Type: application/json - {"message":"API key deleted"} +{"message":"API key deleted"} +``` \ No newline at end of file diff --git a/docs/sources/http_api/dashboard.md b/docs/sources/http_api/dashboard.md index 899c76ce6e0..300e5613db4 100644 --- a/docs/sources/http_api/dashboard.md +++ b/docs/sources/http_api/dashboard.md @@ -158,53 +158,57 @@ Will return the home dashboard. **Example Request**: - GET /api/dashboards/home HTTP/1.1 - Accept: application/json - Content-Type: application/json - Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk +```http +GET /api/dashboards/home HTTP/1.1 +Accept: application/json +Content-Type: application/json +Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk +``` **Example Response**: - HTTP/1.1 200 - Content-Type: application/json +```http +HTTP/1.1 200 +Content-Type: application/json +{ + "meta": { + "isHome":true, + "canSave":false, + "canEdit":false, + "canStar":false, + "slug":"", + "expires":"0001-01-01T00:00:00Z", + "created":"0001-01-01T00:00:00Z" + }, + "dashboard": { + "editable":false, + "hideControls":true, + "nav":[ { - "meta": { - "isHome":true, - "canSave":false, - "canEdit":false, - "canStar":false, - "slug":"", - "expires":"0001-01-01T00:00:00Z", - "created":"0001-01-01T00:00:00Z" - }, - "dashboard": { - "editable":false, - "hideControls":true, - "nav":[ - { - "enable":false, - "type":"timepicker" - } - ], - "rows": [ - { - - } - ], - "style":"dark", - "tags":[], - "templating":{ - "list":[ - ] - }, - "time":{ - }, - "timezone":"browser", - "title":"Home", - "version":5 - } + "enable":false, + "type":"timepicker" } + ], + "rows": [ + { + + } + ], + "style":"dark", + "tags":[], + "templating":{ + "list":[ + ] + }, + "time":{ + }, + "timezone":"browser", + "title":"Home", + "version":5 + } +} +``` ## Tags for Dashboard @@ -215,26 +219,30 @@ Get all tags of dashboards **Example Request**: - GET /api/dashboards/tags HTTP/1.1 - Accept: application/json - Content-Type: application/json - Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk +```http +GET /api/dashboards/tags HTTP/1.1 +Accept: application/json +Content-Type: application/json +Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk +``` **Example Response**: - HTTP/1.1 200 - Content-Type: application/json +```http +HTTP/1.1 200 +Content-Type: application/json - [ - { - "term":"tag1", - "count":1 - }, - { - "term":"tag2", - "count":4 - } - ] +[ + { + "term":"tag1", + "count":1 + }, + { + "term":"tag2", + "count":4 + } +] +``` ## Search Dashboards @@ -249,23 +257,27 @@ Query parameters: **Example Request**: - GET /api/search?query=MyDashboard&starred=true&tag=prod HTTP/1.1 - Accept: application/json - Content-Type: application/json - Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk +```http +GET /api/search?query=MyDashboard&starred=true&tag=prod HTTP/1.1 +Accept: application/json +Content-Type: application/json +Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk +``` **Example Response**: - HTTP/1.1 200 - Content-Type: application/json +```http +HTTP/1.1 200 +Content-Type: application/json - [ - { - "id":1, - "title":"Production Overview", - "uri":"db/production-overview", - "type":"dash-db", - "tags":[], - "isStarred":false - } - ] +[ + { + "id":1, + "title":"Production Overview", + "uri":"db/production-overview", + "type":"dash-db", + "tags":[], + "isStarred":false + } +] +``` \ No newline at end of file diff --git a/docs/sources/http_api/data_source.md b/docs/sources/http_api/data_source.md index 62b09fb5d2c..364b55b0cfc 100644 --- a/docs/sources/http_api/data_source.md +++ b/docs/sources/http_api/data_source.md @@ -18,34 +18,38 @@ parent = "http_api" **Example Request**: - GET /api/datasources HTTP/1.1 - Accept: application/json - Content-Type: application/json - Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk +```http +GET /api/datasources HTTP/1.1 +Accept: application/json +Content-Type: application/json +Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk +``` **Example Response**: - HTTP/1.1 200 - Content-Type: application/json +```http +HTTP/1.1 200 +Content-Type: application/json - [ - { - "id":1, - "orgId":1, - "name":"datasource_elastic", - "type":"elasticsearch", - "access":"proxy", - "url":"http://mydatasource.com", - "password":"", - "user":"", - "database":"grafana-dash", - "basicAuth":false, - "basicAuthUser":"", - "basicAuthPassword":"", - "isDefault":false, - "jsonData":null - } - ] +[ + { + "id":1, + "orgId":1, + "name":"datasource_elastic", + "type":"elasticsearch", + "access":"proxy", + "url":"http://mydatasource.com", + "password":"", + "user":"", + "database":"grafana-dash", + "basicAuth":false, + "basicAuthUser":"", + "basicAuthPassword":"", + "isDefault":false, + "jsonData":null + } +] +``` ## Get a single data sources by Id @@ -53,32 +57,36 @@ parent = "http_api" **Example Request**: - GET /api/datasources/1 HTTP/1.1 - Accept: application/json - Content-Type: application/json - Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk +```http +GET /api/datasources/1 HTTP/1.1 +Accept: application/json +Content-Type: application/json +Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk +``` **Example Response**: - HTTP/1.1 200 - Content-Type: application/json +```http +HTTP/1.1 200 +Content-Type: application/json - { - "id":1, - "orgId":1, - "name":"test_datasource", - "type":"graphite", - "access":"proxy", - "url":"http://mydatasource.com", - "password":"", - "user":"", - "database":"", - "basicAuth":false, - "basicAuthUser":"", - "basicAuthPassword":"", - "isDefault":false, - "jsonData":null - } +{ + "id":1, + "orgId":1, + "name":"test_datasource", + "type":"graphite", + "access":"proxy", + "url":"http://mydatasource.com", + "password":"", + "user":"", + "database":"", + "basicAuth":false, + "basicAuthUser":"", + "basicAuthPassword":"", + "isDefault":false, + "jsonData":null +} +``` ## Get a single data source by Name @@ -86,32 +94,36 @@ parent = "http_api" **Example Request**: - GET /api/datasources/name/test_datasource HTTP/1.1 - Accept: application/json - Content-Type: application/json - Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk +```http +GET /api/datasources/name/test_datasource HTTP/1.1 +Accept: application/json +Content-Type: application/json +Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk +``` **Example Response**: - HTTP/1.1 200 - Content-Type: application/json +```http +HTTP/1.1 200 +Content-Type: application/json - { - "id":1, - "orgId":1, - "name":"test_datasource", - "type":"graphite", - "access":"proxy", - "url":"http://mydatasource.com", - "password":"", - "user":"", - "database":"", - "basicAuth":false, - "basicAuthUser":"", - "basicAuthPassword":"", - "isDefault":false, - "jsonData":null - } +{ + "id":1, + "orgId":1, + "name":"test_datasource", + "type":"graphite", + "access":"proxy", + "url":"http://mydatasource.com", + "password":"", + "user":"", + "database":"", + "basicAuth":false, + "basicAuthUser":"", + "basicAuthPassword":"", + "isDefault":false, + "jsonData":null +} +``` ## Get data source Id by Name @@ -119,19 +131,23 @@ parent = "http_api" **Example Request**: - GET /api/datasources/id/test_datasource HTTP/1.1 - Accept: application/json - Content-Type: application/json - Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk +```http +GET /api/datasources/id/test_datasource HTTP/1.1 +Accept: application/json +Content-Type: application/json +Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk +``` **Example Response**: - HTTP/1.1 200 - Content-Type: application/json +```http +HTTP/1.1 200 +Content-Type: application/json - { - "id":1 - } +{ + "id":1 +} +``` ## Create data source @@ -139,48 +155,53 @@ parent = "http_api" **Example Graphite Request**: - POST /api/datasources HTTP/1.1 - Accept: application/json - Content-Type: application/json - Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk +```http +POST /api/datasources HTTP/1.1 +Accept: application/json +Content-Type: application/json +Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk - { - "name":"test_datasource", - "type":"graphite", - "url":"http://mydatasource.com", - "access":"proxy", - "basicAuth":false - } +{ + "name":"test_datasource", + "type":"graphite", + "url":"http://mydatasource.com", + "access":"proxy", + "basicAuth":false +} +``` **Example CloudWatch Request**: - ``` - POST /api/datasources HTTP/1.1 - Accept: application/json - Content-Type: application/json - Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk - - { - "name": "test_datasource", - "type": "cloudwatch", - "url": "http://monitoring.us-west-1.amazonaws.com", - "access": "proxy", - "jsonData": { - "authType": "keys", - "defaultRegion": "us-west-1" - }, - "secureJsonData": { - "accessKey": "Ol4pIDpeKSA6XikgOl4p", - "secretKey": "dGVzdCBrZXkgYmxlYXNlIGRvbid0IHN0ZWFs" - } + +```http +POST /api/datasources HTTP/1.1 +Accept: application/json +Content-Type: application/json +Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk + +{ + "name": "test_datasource", + "type": "cloudwatch", + "url": "http://monitoring.us-west-1.amazonaws.com", + "access": "proxy", + "jsonData": { + "authType": "keys", + "defaultRegion": "us-west-1" + }, + "secureJsonData": { + "accessKey": "Ol4pIDpeKSA6XikgOl4p", + "secretKey": "dGVzdCBrZXkgYmxlYXNlIGRvbid0IHN0ZWFs" } - ``` +} +``` **Example Response**: - HTTP/1.1 200 - Content-Type: application/json +```http +HTTP/1.1 200 +Content-Type: application/json - {"id":1,"message":"Datasource added", "name": "test_datasource"} +{"id":1,"message":"Datasource added", "name": "test_datasource"} +``` ## Update an existing data source @@ -188,34 +209,38 @@ parent = "http_api" **Example Request**: - PUT /api/datasources/1 HTTP/1.1 - Accept: application/json - Content-Type: application/json - Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk +```http +PUT /api/datasources/1 HTTP/1.1 +Accept: application/json +Content-Type: application/json +Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk - { - "id":1, - "orgId":1, - "name":"test_datasource", - "type":"graphite", - "access":"proxy", - "url":"http://mydatasource.com", - "password":"", - "user":"", - "database":"", - "basicAuth":true, - "basicAuthUser":"basicuser", - "basicAuthPassword":"basicuser", - "isDefault":false, - "jsonData":null - } +{ + "id":1, + "orgId":1, + "name":"test_datasource", + "type":"graphite", + "access":"proxy", + "url":"http://mydatasource.com", + "password":"", + "user":"", + "database":"", + "basicAuth":true, + "basicAuthUser":"basicuser", + "basicAuthPassword":"basicuser", + "isDefault":false, + "jsonData":null +} +``` **Example Response**: - HTTP/1.1 200 - Content-Type: application/json +```http +HTTP/1.1 200 +Content-Type: application/json - {"message":"Datasource updated", "id": 1, "name": "test_datasource"} +{"message":"Datasource updated", "id": 1, "name": "test_datasource"} +``` ## Delete an existing data source by id @@ -223,17 +248,21 @@ parent = "http_api" **Example Request**: - DELETE /api/datasources/1 HTTP/1.1 - Accept: application/json - Content-Type: application/json - Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk +```http +DELETE /api/datasources/1 HTTP/1.1 +Accept: application/json +Content-Type: application/json +Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk +``` **Example Response**: - HTTP/1.1 200 - Content-Type: application/json +```http +HTTP/1.1 200 +Content-Type: application/json - {"message":"Data source deleted"} +{"message":"Data source deleted"} +``` ## Delete an existing data source by name @@ -241,17 +270,21 @@ parent = "http_api" **Example Request**: - DELETE /api/datasources/name/test_datasource HTTP/1.1 - Accept: application/json - Content-Type: application/json - Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk +```http +DELETE /api/datasources/name/test_datasource HTTP/1.1 +Accept: application/json +Content-Type: application/json +Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk +``` **Example Response**: - HTTP/1.1 200 - Content-Type: application/json +```http +HTTP/1.1 200 +Content-Type: application/json - {"message":"Data source deleted"} +{"message":"Data source deleted"} +``` ## Data source proxy calls diff --git a/docs/sources/http_api/org.md b/docs/sources/http_api/org.md index 72c995adedf..6542f00fd81 100644 --- a/docs/sources/http_api/org.md +++ b/docs/sources/http_api/org.md @@ -18,20 +18,24 @@ parent = "http_api" **Example Request**: - GET /api/org HTTP/1.1 - Accept: application/json - Content-Type: application/json - Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk +```http +GET /api/org HTTP/1.1 +Accept: application/json +Content-Type: application/json +Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk +``` **Example Response**: - HTTP/1.1 200 - Content-Type: application/json +```http +HTTP/1.1 200 +Content-Type: application/json - { - "id":1, - "name":"Main Org." - } +{ + "id":1, + "name":"Main Org." +} +``` ## Get Organisation by Id @@ -39,57 +43,64 @@ parent = "http_api" **Example Request**: - GET /api/orgs/1 HTTP/1.1 - Accept: application/json - Content-Type: application/json - Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk +```http +GET /api/orgs/1 HTTP/1.1 +Accept: application/json +Content-Type: application/json +Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk +``` **Example Response**: - HTTP/1.1 200 - Content-Type: application/json - - { - "id":1, - "name":"Main Org.", - "address":{ - "address1":"", - "address2":"", - "city":"", - "zipCode":"", - "state":"", - "country":"" - } - } +```http +HTTP/1.1 200 +Content-Type: application/json +{ + "id":1, + "name":"Main Org.", + "address":{ + "address1":"", + "address2":"", + "city":"", + "zipCode":"", + "state":"", + "country":"" + } +} +``` ## Get Organisation by Name `GET /api/orgs/name/:orgName` **Example Request**: - GET /api/orgs/name/Main%20Org%2E HTTP/1.1 - Accept: application/json - Content-Type: application/json - Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk +```http +GET /api/orgs/name/Main%20Org%2E HTTP/1.1 +Accept: application/json +Content-Type: application/json +Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk +``` **Example Response**: - HTTP/1.1 200 - Content-Type: application/json +```http +HTTP/1.1 200 +Content-Type: application/json - { - "id":1, - "name":"Main Org.", - "address":{ - "address1":"", - "address2":"", - "city":"", - "zipCode":"", - "state":"", - "country":"" - } - } +{ + "id":1, + "name":"Main Org.", + "address":{ + "address1":"", + "address2":"", + "city":"", + "zipCode":"", + "state":"", + "country":"" + } +} +``` ## Create Organisation @@ -97,26 +108,28 @@ parent = "http_api" **Example Request**: - POST /api/orgs HTTP/1.1 - Accept: application/json - Content-Type: application/json - Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk - - { - "name":"New Org." - } +```http +POST /api/orgs HTTP/1.1 +Accept: application/json +Content-Type: application/json +Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk +{ + "name":"New Org." +} +``` **Example Response**: - HTTP/1.1 200 - Content-Type: application/json - - { - "orgId":"1", - "message":"Organization created" - } +```http +HTTP/1.1 200 +Content-Type: application/json +{ + "orgId":"1", + "message":"Organization created" +} +``` ## Update current Organisation @@ -125,23 +138,25 @@ parent = "http_api" **Example Request**: - PUT /api/org HTTP/1.1 - Accept: application/json - Content-Type: application/json - Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk - - { - "name":"Main Org." - } +```http +PUT /api/org HTTP/1.1 +Accept: application/json +Content-Type: application/json +Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk +{ + "name":"Main Org." +} +``` **Example Response**: - HTTP/1.1 200 - Content-Type: application/json - - {"message":"Organization updated"} +```http +HTTP/1.1 200 +Content-Type: application/json +{"message":"Organization updated"} +``` ## Get all users within the actual organisation @@ -149,25 +164,29 @@ parent = "http_api" **Example Request**: - GET /api/org/users HTTP/1.1 - Accept: application/json - Content-Type: application/json - Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk +```http +GET /api/org/users HTTP/1.1 +Accept: application/json +Content-Type: application/json +Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk +``` **Example Response**: - HTTP/1.1 200 - Content-Type: application/json +```http +HTTP/1.1 200 +Content-Type: application/json - [ - { - "orgId":1, - "userId":1, - "email":"admin@mygraf.com", - "login":"admin", - "role":"Admin" - } - ] +[ + { + "orgId":1, + "userId":1, + "email":"admin@mygraf.com", + "login":"admin", + "role":"Admin" + } +] +``` ## Add a new user to the actual organisation @@ -177,23 +196,26 @@ Adds a global user to the actual organisation. **Example Request**: - POST /api/org/users HTTP/1.1 - Accept: application/json - Content-Type: application/json - Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk - - { - "role": "Admin", - "loginOrEmail": "admin" - } +```http +POST /api/org/users HTTP/1.1 +Accept: application/json +Content-Type: application/json +Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk +{ + "role": "Admin", + "loginOrEmail": "admin" +} +``` **Example Response**: - HTTP/1.1 200 - Content-Type: application/json +```http +HTTP/1.1 200 +Content-Type: application/json - {"message":"User added to organization"} +{"message":"User added to organization"} +``` ## Updates the given user @@ -201,23 +223,25 @@ Adds a global user to the actual organisation. **Example Request**: - PATCH /api/org/users/1 HTTP/1.1 - Accept: application/json - Content-Type: application/json - Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk - - { - "role": "Viewer", - } +```http +PATCH /api/org/users/1 HTTP/1.1 +Accept: application/json +Content-Type: application/json +Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk +{ + "role": "Viewer", +} +``` **Example Response**: - HTTP/1.1 200 - Content-Type: application/json - - {"message":"Organization user updated"} +```http +HTTP/1.1 200 +Content-Type: application/json +{"message":"Organization user updated"} +``` ## Delete user in actual organisation @@ -225,18 +249,21 @@ Adds a global user to the actual organisation. **Example Request**: - DELETE /api/org/users/1 HTTP/1.1 - Accept: application/json - Content-Type: application/json - Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk +```http +DELETE /api/org/users/1 HTTP/1.1 +Accept: application/json +Content-Type: application/json +Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk +``` **Example Response**: - HTTP/1.1 200 - Content-Type: application/json - - {"message":"User removed from organization"} +```http +HTTP/1.1 200 +Content-Type: application/json +{"message":"User removed from organization"} +``` # Organisations @@ -246,22 +273,26 @@ Adds a global user to the actual organisation. **Example Request**: - GET /api/orgs HTTP/1.1 - Accept: application/json - Content-Type: application/json - Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk +```http +GET /api/orgs HTTP/1.1 +Accept: application/json +Content-Type: application/json +Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk +``` **Example Response**: - HTTP/1.1 200 - Content-Type: application/json +```http +HTTP/1.1 200 +Content-Type: application/json - [ - { - "id":1, - "name":"Main Org." - } - ] +[ + { + "id":1, + "name":"Main Org." + } +] +``` ## Update Organisation @@ -271,22 +302,25 @@ Update Organisation, fields *Adress 1*, *Adress 2*, *City* are not implemented y **Example Request**: - PUT /api/orgs/1 HTTP/1.1 - Accept: application/json - Content-Type: application/json - Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk - - { - "name":"Main Org 2." - } +```http +PUT /api/orgs/1 HTTP/1.1 +Accept: application/json +Content-Type: application/json +Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk +{ + "name":"Main Org 2." +} +``` **Example Response**: - HTTP/1.1 200 - Content-Type: application/json +```http +HTTP/1.1 200 +Content-Type: application/json - {"message":"Organization updated"} +{"message":"Organization updated"} +``` ## Get Users in Organisation @@ -294,24 +328,28 @@ Update Organisation, fields *Adress 1*, *Adress 2*, *City* are not implemented y **Example Request**: - GET /api/orgs/1/users HTTP/1.1 - Accept: application/json - Content-Type: application/json - Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk +```http +GET /api/orgs/1/users HTTP/1.1 +Accept: application/json +Content-Type: application/json +Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk +``` **Example Response**: - HTTP/1.1 200 - Content-Type: application/json - [ - { - "orgId":1, - "userId":1, - "email":"admin@mygraf.com", - "login":"admin", - "role":"Admin" - } - ] +```http +HTTP/1.1 200 +Content-Type: application/json +[ + { + "orgId":1, + "userId":1, + "email":"admin@mygraf.com", + "login":"admin", + "role":"Admin" + } +] +``` ## Add User in Organisation @@ -319,22 +357,26 @@ Update Organisation, fields *Adress 1*, *Adress 2*, *City* are not implemented y **Example Request**: - POST /api/orgs/1/users HTTP/1.1 - Accept: application/json - Content-Type: application/json - Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk +```http +POST /api/orgs/1/users HTTP/1.1 +Accept: application/json +Content-Type: application/json +Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk - { - "loginOrEmail":"user", - "role":"Viewer" - } +{ + "loginOrEmail":"user", + "role":"Viewer" +} +``` **Example Response**: - HTTP/1.1 200 - Content-Type: application/json +```http +HTTP/1.1 200 +Content-Type: application/json - {"message":"User added to organization"} +{"message":"User added to organization"} +``` ## Update Users in Organisation @@ -342,21 +384,25 @@ Update Organisation, fields *Adress 1*, *Adress 2*, *City* are not implemented y **Example Request**: - PATCH /api/orgs/1/users/2 HTTP/1.1 - Accept: application/json - Content-Type: application/json - Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk +```http +PATCH /api/orgs/1/users/2 HTTP/1.1 +Accept: application/json +Content-Type: application/json +Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk - { - "role":"Admin" - } +{ + "role":"Admin" +} +``` **Example Response**: - HTTP/1.1 200 - Content-Type: application/json +```http +HTTP/1.1 200 +Content-Type: application/json - {"message":"Organization user updated"} +{"message":"Organization user updated"} +``` ## Delete User in Organisation @@ -364,14 +410,18 @@ Update Organisation, fields *Adress 1*, *Adress 2*, *City* are not implemented y **Example Request**: - DELETE /api/orgs/1/users/2 HTTP/1.1 - Accept: application/json - Content-Type: application/json - Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk +```http +DELETE /api/orgs/1/users/2 HTTP/1.1 +Accept: application/json +Content-Type: application/json +Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk +``` **Example Response**: - HTTP/1.1 200 - Content-Type: application/json +```http +HTTP/1.1 200 +Content-Type: application/json - {"message":"User removed from organization"} +{"message":"User removed from organization"} +``` \ No newline at end of file diff --git a/docs/sources/http_api/other.md b/docs/sources/http_api/other.md index 65d18f94ea4..5bf0cde05fe 100644 --- a/docs/sources/http_api/other.md +++ b/docs/sources/http_api/other.md @@ -18,43 +18,47 @@ parent = "http_api" **Example Request**: - GET /api/frontend/settings HTTP/1.1 - Accept: application/json - Content-Type: application/json - Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk +```http +GET /api/frontend/settings HTTP/1.1 +Accept: application/json +Content-Type: application/json +Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk +``` **Example Response**: - HTTP/1.1 200 - Content-Type: application/json +```http +HTTP/1.1 200 +Content-Type: application/json - { - "allowOrgCreate":true, - "appSubUrl":"", - "buildInfo":{ - "buildstamp":xxxxxx, - "commit":"vyyyy", - "version":"zzzzz" - }, - "datasources":{ - "datasourcename":{ - "index":"grafana-dash", - "meta":{ - "annotations":true, - "module":"plugins/datasource/grafana/datasource", - "name":"Grafana", - "partials":{ - "annotations":"app/plugins/datasource/grafana/partials/annotations.editor.html", - "config":"app/plugins/datasource/grafana/partials/config.html" - }, - "pluginType":"datasource", - "serviceName":"Grafana", - "type":"grafanasearch" - } - } - }, - "defaultDatasource": "Grafana" +{ + "allowOrgCreate":true, + "appSubUrl":"", + "buildInfo":{ + "buildstamp":xxxxxx, + "commit":"vyyyy", + "version":"zzzzz" + }, + "datasources":{ + "datasourcename":{ + "index":"grafana-dash", + "meta":{ + "annotations":true, + "module":"plugins/datasource/grafana/datasource", + "name":"Grafana", + "partials":{ + "annotations":"app/plugins/datasource/grafana/partials/annotations.editor.html", + "config":"app/plugins/datasource/grafana/partials/config.html" + }, + "pluginType":"datasource", + "serviceName":"Grafana", + "type":"grafanasearch" + } } + }, + "defaultDatasource": "Grafana" +} +``` # Login API @@ -64,14 +68,18 @@ parent = "http_api" **Example Request**: - GET /api/login/ping HTTP/1.1 - Accept: application/json - Content-Type: application/json - Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk +```http +GET /api/login/ping HTTP/1.1 +Accept: application/json +Content-Type: application/json +Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk +``` **Example Response**: - HTTP/1.1 200 - Content-Type: application/json +```http +HTTP/1.1 200 +Content-Type: application/json - {"message": "Logged in"} +{"message": "Logged in"} +``` \ No newline at end of file diff --git a/docs/sources/http_api/preferences.md b/docs/sources/http_api/preferences.md index a4b953cdaed..ac1d1ee7a0d 100644 --- a/docs/sources/http_api/preferences.md +++ b/docs/sources/http_api/preferences.md @@ -26,17 +26,21 @@ system default value. **Example Request**: - GET /api/user/preferences HTTP/1.1 - Accept: application/json - Content-Type: application/json - Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk +```http +GET /api/user/preferences HTTP/1.1 +Accept: application/json +Content-Type: application/json +Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk +``` **Example Response**: - HTTP/1.1 200 - Content-Type: application/json +```http +HTTP/1.1 200 +Content-Type: application/json - {"theme":"","homeDashboardId":0,"timezone":""} +{"theme":"","homeDashboardId":0,"timezone":""} +``` ## Update Current User Prefs @@ -44,23 +48,27 @@ system default value. **Example Request**: - PUT /api/user/preferences HTTP/1.1 - Accept: application/json - Content-Type: application/json - Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk +```http +PUT /api/user/preferences HTTP/1.1 +Accept: application/json +Content-Type: application/json +Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk - { - "theme": "", - "homeDashboardId":0, - "timezone":"utc" - } +{ + "theme": "", + "homeDashboardId":0, + "timezone":"utc" +} +``` **Example Response**: - HTTP/1.1 200 - Content-Type: text/plain; charset=utf-8 +```http +HTTP/1.1 200 +Content-Type: text/plain; charset=utf-8 - {"message":"Preferences updated"} +{"message":"Preferences updated"} +``` ## Get Current Org Prefs @@ -68,17 +76,21 @@ system default value. **Example Request**: - GET /api/org/preferences HTTP/1.1 - Accept: application/json - Content-Type: application/json - Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk +```http +GET /api/org/preferences HTTP/1.1 +Accept: application/json +Content-Type: application/json +Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk +``` **Example Response**: - HTTP/1.1 200 - Content-Type: application/json +```http +HTTP/1.1 200 +Content-Type: application/json - {"theme":"","homeDashboardId":0,"timezone":""} +{"theme":"","homeDashboardId":0,"timezone":""} +``` ## Update Current Org Prefs @@ -86,20 +98,24 @@ system default value. **Example Request**: - PUT /api/org/preferences HTTP/1.1 - Accept: application/json - Content-Type: application/json - Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk +```http +PUT /api/org/preferences HTTP/1.1 +Accept: application/json +Content-Type: application/json +Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk - { - "theme": "", - "homeDashboardId":0, - "timezone":"utc" - } +{ + "theme": "", + "homeDashboardId":0, + "timezone":"utc" +} +``` **Example Response**: - HTTP/1.1 200 - Content-Type: text/plain; charset=utf-8 +```http +HTTP/1.1 200 +Content-Type: text/plain; charset=utf-8 - {"message":"Preferences updated"} +{"message":"Preferences updated"} +``` \ No newline at end of file diff --git a/docs/sources/http_api/snapshot.md b/docs/sources/http_api/snapshot.md index d466d01e051..5cecdb85fc3 100644 --- a/docs/sources/http_api/snapshot.md +++ b/docs/sources/http_api/snapshot.md @@ -17,6 +17,7 @@ parent = "http_api" **Example Request**: +```http POST /api/snapshots HTTP/1.1 Accept: application/json Content-Type: application/json @@ -51,18 +52,20 @@ parent = "http_api" }, "expires": 3600 } +``` JSON Body schema: - **dashboard** – Required. The complete dashboard model. - **name** – Optional. snapshot name -- **expires** - Optional. When the snapshot should expire in seconds. 3600 is 1 hour, 86400 is 1 day. Default is never to expire. +- **expires** - Optional. When the snapshot should expire in seconds. 3600 is 1 hour, 86400 is 1 day. Default is never to expire. - **external** - Optional. Save the snapshot on an external server rather than locally. Default is `false`. - **key** - Optional. Define the unique key. Required if **external** is `true`. - **deleteKey** - Optional. Unique key used to delete the snapshot. It is different from the **key** so that only the creator can delete the snapshot. Required if **external** is `true`. **Example Response**: +```http HTTP/1.1 200 Content-Type: application/json { @@ -71,6 +74,7 @@ JSON Body schema: "key":"YYYYYYY", "url":"myurl/dashboard/snapshot/YYYYYYY" } +``` Keys: @@ -83,54 +87,58 @@ Keys: **Example Request**: - GET /api/snapshots/YYYYYYY HTTP/1.1 - Accept: application/json - Content-Type: application/json - Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk +```http +GET /api/snapshots/YYYYYYY HTTP/1.1 +Accept: application/json +Content-Type: application/json +Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk +``` **Example Response**: - HTTP/1.1 200 - Content-Type: application/json +```http +HTTP/1.1 200 +Content-Type: application/json - { - "meta":{ - "isSnapshot":true, - "type":"snapshot", - "canSave":false, - "canEdit":false, - "canStar":false, - "slug":"", - "expires":"2200-13-32T25:23:23+02:00", - "created":"2200-13-32T28:24:23+02:00" - }, - "dashboard": { - "editable":false, - "hideControls":true, - "nav":[ - { - "enable":false, - "type":"timepicker" - } - ], - "rows": [ - { +{ + "meta":{ + "isSnapshot":true, + "type":"snapshot", + "canSave":false, + "canEdit":false, + "canStar":false, + "slug":"", + "expires":"2200-13-32T25:23:23+02:00", + "created":"2200-13-32T28:24:23+02:00" + }, + "dashboard": { + "editable":false, + "hideControls":true, + "nav": [ + { + "enable":false, + "type":"timepicker" + } + ], + "rows": [ + { - } - ], - "style":"dark", - "tags":[], - "templating":{ - "list":[ - ] - }, - "time":{ - }, - "timezone":"browser", - "title":"Home", - "version":5 - } - } + } + ], + "style":"dark", + "tags":[], + "templating":{ + "list":[ + ] + }, + "time":{ + }, + "timezone":"browser", + "title":"Home", + "version":5 + } +} +``` ## Delete Snapshot by Id @@ -138,14 +146,18 @@ Keys: **Example Request**: - GET /api/snapshots/YYYYYYY HTTP/1.1 - Accept: application/json - Content-Type: application/json - Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk +```http +GET /api/snapshots/YYYYYYY HTTP/1.1 +Accept: application/json +Content-Type: application/json +Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk +``` **Example Response**: - HTTP/1.1 200 - Content-Type: application/json +```http +HTTP/1.1 200 +Content-Type: application/json - {"message":"Snapshot deleted. It might take an hour before it's cleared from a CDN cache."} +{"message":"Snapshot deleted. It might take an hour before it's cleared from a CDN cache."} +``` \ No newline at end of file diff --git a/docs/sources/http_api/user.md b/docs/sources/http_api/user.md index 761ac938cd8..ba8afd4db22 100644 --- a/docs/sources/http_api/user.md +++ b/docs/sources/http_api/user.md @@ -17,34 +17,38 @@ parent = "http_api" **Example Request**: - GET /api/users HTTP/1.1 - Accept: application/json - Content-Type: application/json - Authorization: Basic YWRtaW46YWRtaW4= +```http +GET /api/users HTTP/1.1 +Accept: application/json +Content-Type: application/json +Authorization: Basic YWRtaW46YWRtaW4= +``` Default value for the `perpage` parameter is `1000` and for the `page` parameter is `1`. Requires basic authentication and that the authenticated user is a Grafana Admin. **Example Response**: - HTTP/1.1 200 - Content-Type: application/json +```http +HTTP/1.1 200 +Content-Type: application/json - [ - { - "id": 1, - "name": "Admin", - "login": "admin", - "email": "admin@mygraf.com", - "isAdmin": true - }, - { - "id": 2, - "name": "User", - "login": "user", - "email": "user@mygraf.com", - "isAdmin": false - } - ] +[ + { + "id": 1, + "name": "Admin", + "login": "admin", + "email": "admin@mygraf.com", + "isAdmin": true + }, + { + "id": 2, + "name": "User", + "login": "user", + "email": "user@mygraf.com", + "isAdmin": false + } +] +``` ## Search Users with Paging @@ -52,10 +56,12 @@ Default value for the `perpage` parameter is `1000` and for the `page` parameter **Example Request**: - GET /api/users/search?perpage=10&page=1&query=mygraf HTTP/1.1 - Accept: application/json - Content-Type: application/json - Authorization: Basic YWRtaW46YWRtaW4= +```http +GET /api/users/search?perpage=10&page=1&query=mygraf HTTP/1.1 +Accept: application/json +Content-Type: application/json +Authorization: Basic YWRtaW46YWRtaW4= +``` Default value for the `perpage` parameter is `1000` and for the `page` parameter is `1`. The `totalCount` field in the response can be used for pagination of the user list E.g. if `totalCount` is equal to 100 users and the `perpage` parameter is set to 10 then there are 10 pages of users. The `query` parameter is optional and it will return results where the query value is contained in one of the `name`, `login` or `email` fields. Query values with spaces need to be url encoded e.g. `query=Jane%20Doe`. @@ -63,29 +69,31 @@ Requires basic authentication and that the authenticated user is a Grafana Admin **Example Response**: - HTTP/1.1 200 - Content-Type: application/json +```http +HTTP/1.1 200 +Content-Type: application/json +{ + "totalCount": 2, + "users": [ { - "totalCount": 2, - "users": [ - { - "id": 1, - "name": "Admin", - "login": "admin", - "email": "admin@mygraf.com", - "isAdmin": true - }, - { - "id": 2, - "name": "User", - "login": "user", - "email": "user@mygraf.com", - "isAdmin": false - } - ], - "page": 1, - "perPage": 10 + "id": 1, + "name": "Admin", + "login": "admin", + "email": "admin@mygraf.com", + "isAdmin": true + }, + { + "id": 2, + "name": "User", + "login": "user", + "email": "user@mygraf.com", + "isAdmin": false } + ], + "page": 1, + "perPage": 10 +} +``` ## Get single user by Id @@ -93,26 +101,29 @@ Requires basic authentication and that the authenticated user is a Grafana Admin **Example Request**: - GET /api/users/1 HTTP/1.1 - Accept: application/json - Content-Type: application/json - Authorization: Basic YWRtaW46YWRtaW4= - +```http +GET /api/users/1 HTTP/1.1 +Accept: application/json +Content-Type: application/json +Authorization: Basic YWRtaW46YWRtaW4= +``` Requires basic authentication and that the authenticated user is a Grafana Admin. **Example Response**: - HTTP/1.1 200 - Content-Type: application/json +```http +HTTP/1.1 200 +Content-Type: application/json - { - "email": "user@mygraf.com" - "name": "admin", - "login": "admin", - "theme": "light", - "orgId": 1, - "isGrafanaAdmin": true - } +{ + "email": "user@mygraf.com" + "name": "admin", + "login": "admin", + "theme": "light", + "orgId": 1, + "isGrafanaAdmin": true +} +``` ## Get single user by Username(login) or Email @@ -120,34 +131,39 @@ Requires basic authentication and that the authenticated user is a Grafana Admin **Example Request using the email as option**: - GET /api/users/lookup?loginOrEmail=user@mygraf.com HTTP/1.1 - Accept: application/json - Content-Type: application/json - Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk +```http +GET /api/users/lookup?loginOrEmail=user@mygraf.com HTTP/1.1 +Accept: application/json +Content-Type: application/json +Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk +``` **Example Request using the username as option**: - - GET /api/users/lookup?loginOrEmail=admin HTTP/1.1 - Accept: application/json - Content-Type: application/json - Authorization: Basic YWRtaW46YWRtaW4= + +```http +GET /api/users/lookup?loginOrEmail=admin HTTP/1.1 +Accept: application/json +Content-Type: application/json +Authorization: Basic YWRtaW46YWRtaW4= +``` Requires basic authentication and that the authenticated user is a Grafana Admin. **Example Response**: - HTTP/1.1 200 - Content-Type: application/json - - { - "email": "user@mygraf.com" - "name": "admin", - "login": "admin", - "theme": "light", - "orgId": 1, - "isGrafanaAdmin": true - } +```http +HTTP/1.1 200 +Content-Type: application/json +{ + "email": "user@mygraf.com" + "name": "admin", + "login": "admin", + "theme": "light", + "orgId": 1, + "isGrafanaAdmin": true +} +``` ## User Update @@ -155,27 +171,30 @@ Requires basic authentication and that the authenticated user is a Grafana Admin **Example Request**: - PUT /api/users/2 HTTP/1.1 - Accept: application/json - Content-Type: application/json - Authorization: Basic YWRtaW46YWRtaW4= +```http +PUT /api/users/2 HTTP/1.1 +Accept: application/json +Content-Type: application/json +Authorization: Basic YWRtaW46YWRtaW4= - { - "email":"user@mygraf.com", - "name":"User2", - "login":"user", - "theme":"light" - } +{ + "email":"user@mygraf.com", + "name":"User2", + "login":"user", + "theme":"light" +} +``` Requires basic authentication and that the authenticated user is a Grafana Admin. **Example Response**: - HTTP/1.1 200 - Content-Type: application/json - - {"message":"User updated"} +```http +HTTP/1.1 200 +Content-Type: application/json +{"message":"User updated"} +``` ## Get Organisations for user @@ -183,25 +202,29 @@ Requires basic authentication and that the authenticated user is a Grafana Admin **Example Request**: - GET /api/users/1/orgs HTTP/1.1 - Accept: application/json - Content-Type: application/json - Authorization: Basic YWRtaW46YWRtaW4= +```http +GET /api/users/1/orgs HTTP/1.1 +Accept: application/json +Content-Type: application/json +Authorization: Basic YWRtaW46YWRtaW4= +``` Requires basic authentication and that the authenticated user is a Grafana Admin. **Example Response**: - HTTP/1.1 200 - Content-Type: application/json +```http +HTTP/1.1 200 +Content-Type: application/json - [ - { - "orgId":1, - "name":"Main Org.", - "role":"Admin" - } - ] +[ + { + "orgId":1, + "name":"Main Org.", + "role":"Admin" + } +] +``` ## User @@ -211,24 +234,28 @@ Requires basic authentication and that the authenticated user is a Grafana Admin **Example Request**: - GET /api/user HTTP/1.1 - Accept: application/json - Content-Type: application/json - Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk +```http +GET /api/user HTTP/1.1 +Accept: application/json +Content-Type: application/json +Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk +``` **Example Response**: - HTTP/1.1 200 - Content-Type: application/json +```http +HTTP/1.1 200 +Content-Type: application/json - { - "email":"admin@mygraf.com", - "name":"Admin", - "login":"admin", - "theme":"light", - "orgId":1, - "isGrafanaAdmin":true - } +{ + "email":"admin@mygraf.com", + "name":"Admin", + "login":"admin", + "theme":"light", + "orgId":1, + "isGrafanaAdmin":true +} +``` ## Change Password @@ -238,23 +265,27 @@ Changes the password for the user **Example Request**: - PUT /api/user/password HTTP/1.1 - Accept: application/json - Content-Type: application/json - Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk +```http +PUT /api/user/password HTTP/1.1 +Accept: application/json +Content-Type: application/json +Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk - { - "oldPassword": "old_password", - "newPassword": "new_password", - "confirmNew": "confirm_new_password" - } +{ + "oldPassword": "old_password", + "newPassword": "new_password", + "confirmNew": "confirm_new_password" +} +``` **Example Response**: - HTTP/1.1 200 - Content-Type: application/json +```http +HTTP/1.1 200 +Content-Type: application/json - {"message":"User password changed"} +{"message":"User password changed"} +``` ## Switch user context for a specified user @@ -264,15 +295,19 @@ Switch user context to the given organization. Requires basic authentication and **Example Request**: - POST /api/users/7/using/2 HTTP/1.1 - Authorization: Basic YWRtaW46YWRtaW4= +```http +POST /api/users/7/using/2 HTTP/1.1 +Authorization: Basic YWRtaW46YWRtaW4= +``` **Example Response**: - HTTP/1.1 200 - Content-Type: application/json +```http +HTTP/1.1 200 +Content-Type: application/json - {"message":"Active organization changed"} +{"message":"Active organization changed"} +``` ## Switch user context for signed in user @@ -282,17 +317,21 @@ Switch user context to the given organization. **Example Request**: - POST /api/user/using/2 HTTP/1.1 - Accept: application/json - Content-Type: application/json - Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk +```http +POST /api/user/using/2 HTTP/1.1 +Accept: application/json +Content-Type: application/json +Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk +``` **Example Response**: - HTTP/1.1 200 - Content-Type: application/json +```http +HTTP/1.1 200 +Content-Type: application/json - {"message":"Active organization changed"} +{"message":"Active organization changed"} +``` ## Organisations of the actual User @@ -302,23 +341,27 @@ Return a list of all organisations of the current user. **Example Request**: - GET /api/user/orgs HTTP/1.1 - Accept: application/json - Content-Type: application/json - Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk +```http +GET /api/user/orgs HTTP/1.1 +Accept: application/json +Content-Type: application/json +Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk +``` **Example Response**: - HTTP/1.1 200 - Content-Type: application/json +```http +HTTP/1.1 200 +Content-Type: application/json - [ - { - "orgId":1, - "name":"Main Org.", - "role":"Admin" - } - ] +[ + { + "orgId":1, + "name":"Main Org.", + "role":"Admin" + } +] +``` ## Star a dashboard @@ -328,17 +371,21 @@ Stars the given Dashboard for the actual user. **Example Request**: - POST /api/user/stars/dashboard/1 HTTP/1.1 - Accept: application/json - Content-Type: application/json - Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk +```http +POST /api/user/stars/dashboard/1 HTTP/1.1 +Accept: application/json +Content-Type: application/json +Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk +``` **Example Response**: - HTTP/1.1 200 - Content-Type: application/json +```http +HTTP/1.1 200 +Content-Type: application/json - {"message":"Dashboard starred!"} +{"message":"Dashboard starred!"} +``` ## Unstar a dashboard @@ -348,14 +395,18 @@ Deletes the starring of the given Dashboard for the actual user. **Example Request**: - DELETE /api/user/stars/dashboard/1 HTTP/1.1 - Accept: application/json - Content-Type: application/json - Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk +```http +DELETE /api/user/stars/dashboard/1 HTTP/1.1 +Accept: application/json +Content-Type: application/json +Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk +``` **Example Response**: - HTTP/1.1 200 - Content-Type: application/json +```http +HTTP/1.1 200 +Content-Type: application/json - {"message":"Dashboard unstarred"} +{"message":"Dashboard unstarred"} +``` \ No newline at end of file diff --git a/docs/sources/installation/behind_proxy.md b/docs/sources/installation/behind_proxy.md index 0caa31bbfb5..3d89d8b3c2c 100644 --- a/docs/sources/installation/behind_proxy.md +++ b/docs/sources/installation/behind_proxy.md @@ -15,7 +15,7 @@ weight = 1 It should be straight forward to get Grafana up and running behind a reverse proxy. But here are some things that you might run into. Links and redirects will not be rendered correctly unless you set the server.domain setting. -``` +```bash [server] domain = foo.bar ``` @@ -28,14 +28,14 @@ Here are some example configurations for running Grafana behind a reverse proxy. ### Grafana configuration (ex http://foo.bar.com) -``` +```bash [server] domain = foo.bar ``` ### Nginx configuration -``` +```bash server { listen 80; root /usr/share/nginx/www; @@ -50,14 +50,14 @@ server { ### Examples with **sub path** (ex http://foo.bar.com/grafana) #### Grafana configuration with sub path -``` +```bash [server] domain = foo.bar root_url = %(protocol)s://%(domain)s:/grafana ``` #### Nginx configuration with sub path -``` +```bash server { listen 80; root /usr/share/nginx/www; diff --git a/docs/sources/installation/configuration.md b/docs/sources/installation/configuration.md index 6630825e803..370509f0a63 100644 --- a/docs/sources/installation/configuration.md +++ b/docs/sources/installation/configuration.md @@ -37,26 +37,31 @@ A common problem is forgetting to uncomment a line in the `custom.ini` (or `graf All options in the configuration file (listed below) can be overridden using environment variables using the syntax: - GF__ +```bash +GF__ +``` Where the section name is the text within the brackets. Everything should be upper case, `.` should be replaced by `_`. For example, given these configuration settings: - # default section - instance_name = ${HOSTNAME} +```bash +# default section +instance_name = ${HOSTNAME} - [security] - admin_user = admin - - [auth.google] - client_secret = 0ldS3cretKey +[security] +admin_user = admin +[auth.google] +client_secret = 0ldS3cretKey +``` Then you can override them using: - export GF_DEFAULT_INSTANCE_NAME=my-instance - export GF_SECURITY_ADMIN_USER=true - export GF_AUTH_GOOGLE_CLIENT_SECRET=newS3cretKey +```bash +export GF_DEFAULT_INSTANCE_NAME=my-instance +export GF_SECURITY_ADMIN_USER=true +export GF_AUTH_GOOGLE_CLIENT_SECRET=newS3cretKey +```
      @@ -93,11 +98,15 @@ The IP address to bind to. If empty will bind to all interfaces The port to bind to, defaults to `3000`. To use port 80 you need to either give the Grafana binary permission for example: - $ sudo setcap 'cap_net_bind_service=+ep' /usr/sbin/grafana-server +```bash +$ sudo setcap 'cap_net_bind_service=+ep' /usr/sbin/grafana-server +``` Or redirect port 80 to the Grafana port using: - $ sudo iptables -t nat -A PREROUTING -p tcp --dport 80 -j REDIRECT --to-port 3000 +```bash +$ sudo iptables -t nat -A PREROUTING -p tcp --dport 80 -j REDIRECT --to-port 3000 +``` Another way is put a webserver like Nginx or Apache in front of Grafana and have them proxy requests to Grafana. @@ -312,7 +321,9 @@ You need to create a GitHub OAuth application (you find this under the GitHub settings page). When you create the application you will need to specify a callback URL. Specify this as callback: - http://:/login/github +```bash +http://:/login/github +``` This callback URL must match the full HTTP address that you use in your browser to access Grafana, but with the prefix path of `/login/github`. @@ -320,17 +331,19 @@ When the GitHub OAuth application is created you will get a Client ID and a Client Secret. Specify these in the Grafana configuration file. For example: - [auth.github] - enabled = true - allow_sign_up = true - client_id = YOUR_GITHUB_APP_CLIENT_ID - client_secret = YOUR_GITHUB_APP_CLIENT_SECRET - scopes = user:email - auth_url = https://github.com/login/oauth/authorize - token_url = https://github.com/login/oauth/access_token - api_url = https://api.github.com/user - team_ids = - allowed_organizations = +```bash +[auth.github] +enabled = true +allow_sign_up = true +client_id = YOUR_GITHUB_APP_CLIENT_ID +client_secret = YOUR_GITHUB_APP_CLIENT_SECRET +scopes = user:email +auth_url = https://github.com/login/oauth/authorize +token_url = https://github.com/login/oauth/access_token +api_url = https://api.github.com/user +team_ids = +allowed_organizations = +``` Restart the Grafana back-end. You should now see a GitHub login button on the login page. You can now login or sign up with your GitHub @@ -348,15 +361,17 @@ GitHub. If the authenticated user isn't a member of at least one of the teams they will not be able to register or authenticate with your Grafana instance. For example: - [auth.github] - enabled = true - client_id = YOUR_GITHUB_APP_CLIENT_ID - client_secret = YOUR_GITHUB_APP_CLIENT_SECRET - scopes = user:email,read:org - team_ids = 150,300 - auth_url = https://github.com/login/oauth/authorize - token_url = https://github.com/login/oauth/access_token - allow_sign_up = true +```bash +[auth.github] +enabled = true +client_id = YOUR_GITHUB_APP_CLIENT_ID +client_secret = YOUR_GITHUB_APP_CLIENT_SECRET +scopes = user:email,read:org +team_ids = 150,300 +auth_url = https://github.com/login/oauth/authorize +token_url = https://github.com/login/oauth/access_token +allow_sign_up = true +``` ### allowed_organizations @@ -365,16 +380,18 @@ organizations on GitHub. If the authenticated user isn't a member of at least one of the organizations they will not be able to register or authenticate with your Grafana instance. For example - [auth.github] - enabled = true - client_id = YOUR_GITHUB_APP_CLIENT_ID - client_secret = YOUR_GITHUB_APP_CLIENT_SECRET - scopes = user:email,read:org - auth_url = https://github.com/login/oauth/authorize - token_url = https://github.com/login/oauth/access_token - allow_sign_up = true - # space-delimited organization names - allowed_organizations = github google +```bash +[auth.github] +enabled = true +client_id = YOUR_GITHUB_APP_CLIENT_ID +client_secret = YOUR_GITHUB_APP_CLIENT_SECRET +scopes = user:email,read:org +auth_url = https://github.com/login/oauth/authorize +token_url = https://github.com/login/oauth/access_token +allow_sign_up = true +# space-delimited organization names +allowed_organizations = github google +```
      @@ -385,22 +402,26 @@ Developer Console](https://console.developers.google.com/project). When you create the project you will need to specify a callback URL. Specify this as callback: - http://:/login/google +```bash +http://:/login/google +``` This callback URL must match the full HTTP address that you use in your browser to access Grafana, but with the prefix path of `/login/google`. When the Google project is created you will get a Client ID and a Client Secret. Specify these in the Grafana configuration file. For example: - [auth.google] - enabled = true - client_id = YOUR_GOOGLE_APP_CLIENT_ID - client_secret = YOUR_GOOGLE_APP_CLIENT_SECRET - scopes = https://www.googleapis.com/auth/userinfo.profile https://www.googleapis.com/auth/userinfo.email - auth_url = https://accounts.google.com/o/oauth2/auth - token_url = https://accounts.google.com/o/oauth2/token - allowed_domains = mycompany.com mycompany.org - allow_sign_up = true +```bash +[auth.google] +enabled = true +client_id = YOUR_GOOGLE_APP_CLIENT_ID +client_secret = YOUR_GOOGLE_APP_CLIENT_SECRET +scopes = https://www.googleapis.com/auth/userinfo.profile https://www.googleapis.com/auth/userinfo.email +auth_url = https://accounts.google.com/o/oauth2/auth +token_url = https://accounts.google.com/o/oauth2/token +allowed_domains = mycompany.com mycompany.org +allow_sign_up = true +``` Restart the Grafana back-end. You should now see a Google login button on the login page. You can now login or sign up with your Google @@ -418,16 +439,18 @@ This option could be used if have your own oauth service. This callback URL must match the full HTTP address that you use in your browser to access Grafana, but with the prefix path of `/login/generic_oauth`. - [auth.generic_oauth] - enabled = true - client_id = YOUR_APP_CLIENT_ID - client_secret = YOUR_APP_CLIENT_SECRET - scopes = - auth_url = - token_url = - api_url = - allowed_domains = mycompany.com mycompany.org - allow_sign_up = true +```bash +[auth.generic_oauth] +enabled = true +client_id = YOUR_APP_CLIENT_ID +client_secret = YOUR_APP_CLIENT_SECRET +scopes = +auth_url = +token_url = +api_url = +allowed_domains = mycompany.com mycompany.org +allow_sign_up = true +``` Set api_url to the resource that returns [OpenID UserInfo](https://connect2id.com/products/server/docs/api/userinfo) compatible information. @@ -503,21 +526,25 @@ session table manually. Mysql Example: - CREATE TABLE `session` ( - `key` CHAR(16) NOT NULL, - `data` BLOB, - `expiry` INT(11) UNSIGNED NOT NULL, - PRIMARY KEY (`key`) - ) ENGINE=MyISAM DEFAULT CHARSET=utf8; +```bash +CREATE TABLE `session` ( + `key` CHAR(16) NOT NULL, + `data` BLOB, + `expiry` INT(11) UNSIGNED NOT NULL, + PRIMARY KEY (`key`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8; +``` Postgres Example: - CREATE TABLE session ( - key CHAR(16) NOT NULL, - data BYTEA, - expiry INTEGER NOT NULL, - PRIMARY KEY (key) - ); +```bash +CREATE TABLE session ( + key CHAR(16) NOT NULL, + data BYTEA, + expiry INTEGER NOT NULL, + PRIMARY KEY (key) +); +``` Postgres valid `sslmode` are `disable`, `require` (default), `verify-ca`, and `verify-full`. @@ -698,7 +725,7 @@ Service Account keys can be created and downloaded from https://console.develope Service Account should have "Storage Object Writer" role. ### bucket name -Bucket Name on Google Cloud Storage. +Bucket Name on Google Cloud Storage. ## [alerting] diff --git a/docs/sources/installation/debian.md b/docs/sources/installation/debian.md index 330e3171e86..222a337855a 100644 --- a/docs/sources/installation/debian.md +++ b/docs/sources/installation/debian.md @@ -45,13 +45,17 @@ sudo dpkg -i grafana_4.5.2-beta1_amd64.deb Add the following line to your `/etc/apt/sources.list` file. - deb https://packagecloud.io/grafana/stable/debian/ jessie main +```bash +deb https://packagecloud.io/grafana/stable/debian/ jessie main +``` Use the above line even if you are on Ubuntu or another Debian version. There is also a testing repository if you want beta or release candidates. - deb https://packagecloud.io/grafana/testing/debian/ jessie main +```bash +deb https://packagecloud.io/grafana/testing/debian/ jessie main +``` Then add the [Package Cloud](https://packagecloud.io/grafana) key. This allows you to install signed packages. diff --git a/docs/sources/installation/docker.md b/docs/sources/installation/docker.md index 03e6979d72b..bfb754900fa 100644 --- a/docs/sources/installation/docker.md +++ b/docs/sources/installation/docker.md @@ -14,7 +14,9 @@ weight = 4 Grafana is very easy to install and run using the offical docker container. - $ docker run -d -p 3000:3000 grafana/grafana +```bash +$ docker run -d -p 3000:3000 grafana/grafana +``` All Grafana configuration settings can be defined using environment variables, this is especially useful when using the above container. @@ -26,10 +28,12 @@ folder `/var/lib/grafana` and configuration files is in `/etc/grafana/` folder. You can map these volumes to host folders when you start the container: - $ docker run -d -p 3000:3000 \ - -v /var/lib/grafana:/var/lib/grafana \ - -e "GF_SECURITY_ADMIN_PASSWORD=secret" \ - grafana/grafana +```bash +$ docker run -d -p 3000:3000 \ + -v /var/lib/grafana:/var/lib/grafana \ + -e "GF_SECURITY_ADMIN_PASSWORD=secret" \ + grafana/grafana +``` In the above example I map the data folder and sets a configuration option via an `ENV` instruction. diff --git a/docs/sources/installation/ldap.md b/docs/sources/installation/ldap.md index 769ca3fd1ba..8f6be6e1d8c 100644 --- a/docs/sources/installation/ldap.md +++ b/docs/sources/installation/ldap.md @@ -92,7 +92,7 @@ org_role = "Viewer" By default the configuration expects you to specify a bind DN and bind password. This should be a read only user that can perform LDAP searches. When the user DN is found a second bind is performed with the user provided username & password (in the normal Grafana login form). -``` +```bash bind_dn = "cn=admin,dc=grafana,dc=org" bind_password = "grafana" ``` @@ -102,7 +102,7 @@ bind_password = "grafana" If you can provide a single bind expression that matches all possible users, you can skip the second bind and bind against the user DN directly. This allows you to not specify a bind_password in the configuration file. -``` +```bash bind_dn = "cn=%s,o=users,dc=grafana,dc=org" ``` diff --git a/docs/sources/installation/mac.md b/docs/sources/installation/mac.md index a65c663f398..b1d4f18f699 100644 --- a/docs/sources/installation/mac.md +++ b/docs/sources/installation/mac.md @@ -15,7 +15,7 @@ Installation can be done using [homebrew](http://brew.sh/) Install latest stable: -``` +```bash brew update brew install grafana ``` @@ -24,7 +24,7 @@ To start grafana look at the command printed after the homebrew install complete To upgrade use the reinstall command -``` +```bash brew update brew reinstall grafana ``` @@ -34,13 +34,13 @@ brew reinstall grafana You can also install the latest unstable grafana from git: -``` +```bash brew install --HEAD grafana/grafana/grafana ``` To upgrade grafana if you've installed from HEAD: -``` +```bash brew reinstall --HEAD grafana/grafana/grafana ``` @@ -48,13 +48,13 @@ brew reinstall --HEAD grafana/grafana/grafana To start Grafana using homebrew services first make sure homebrew/services is installed. -``` +```bash brew tap homebrew/services ``` Then start Grafana using: -``` +```bash brew services start grafana ``` diff --git a/docs/sources/installation/rpm.md b/docs/sources/installation/rpm.md index 30b017314c6..b72e5c78d2f 100644 --- a/docs/sources/installation/rpm.md +++ b/docs/sources/installation/rpm.md @@ -26,41 +26,54 @@ installation. You can install Grafana using Yum directly. - $ sudo yum install https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana-4.5.2-1.x86_64.rpm +```bash +$ sudo yum install https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana-4.5.2-1.x86_64.rpm +``` Or install manually using `rpm`. #### On CentOS / Fedora / Redhat: - $ wget https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana-4.5.2-1.x86_64.rpm - $ sudo yum install initscripts fontconfig - $ sudo rpm -Uvh grafana-4.5.2-1.x86_64.rpm +```bash +$ wget https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana-4.5.2-1.x86_64.rpm +$ sudo yum install initscripts fontconfig +$ sudo rpm -Uvh grafana-4.5.2-1.x86_64.rpm +``` #### On OpenSuse: - $ sudo rpm -i --nodeps grafana-4.5.2-1.x86_64.rpm +```bash +$ sudo rpm -i --nodeps grafana-4.5.2-1.x86_64.rpm +``` ## Install via YUM Repository Add the following to a new file at `/etc/yum.repos.d/grafana.repo` - [grafana] - name=grafana - baseurl=https://packagecloud.io/grafana/stable/el/6/$basearch - repo_gpgcheck=1 - enabled=1 - gpgcheck=1 - gpgkey=https://packagecloud.io/gpg.key https://grafanarel.s3.amazonaws.com/RPM-GPG-KEY-grafana - sslverify=1 - sslcacert=/etc/pki/tls/certs/ca-bundle.crt +```bash +[grafana] +name=grafana +baseurl=https://packagecloud.io/grafana/stable/el/6/$basearch +repo_gpgcheck=1 +enabled=1 +gpgcheck=1 +gpgkey=https://packagecloud.io/gpg.key +https://grafanarel.s3.amazonaws.com/RPM-GPG-KEY-grafana +sslverify=1 +sslcacert=/etc/pki/tls/certs/ca-bundle.crt +``` There is also a testing repository if you want beta or release candidates. - baseurl=https://packagecloud.io/grafana/testing/el/6/$basearch +```bash +baseurl=https://packagecloud.io/grafana/testing/el/6/$basearch +``` Then install Grafana via the `yum` command. - $ sudo yum install grafana +```bash +$ sudo yum install grafana +``` ### RPM GPG Key @@ -81,7 +94,9 @@ key](https://grafanarel.s3.amazonaws.com/RPM-GPG-KEY-grafana). You can start Grafana by running: - $ sudo service grafana-server start +```bash +$ sudo service grafana-server start +``` This will start the `grafana-server` process as the `grafana` user, which is created during package installation. The default HTTP port is @@ -89,17 +104,23 @@ which is created during package installation. The default HTTP port is To configure the Grafana server to start at boot time: - $ sudo /sbin/chkconfig --add grafana-server +```bash +$ sudo /sbin/chkconfig --add grafana-server +``` ## Start the server (via systemd) - $ systemctl daemon-reload - $ systemctl start grafana-server - $ systemctl status grafana-server +```bash +$ systemctl daemon-reload +$ systemctl start grafana-server +$ systemctl status grafana-server +``` ### Enable the systemd service to start at boot - sudo systemctl enable grafana-server.service +```bash +sudo systemctl enable grafana-server.service +``` ## Environment file @@ -138,7 +159,7 @@ for example in alert notifications. If the image is missing text make sure you have font packages installed. -``` +```bash yum install fontconfig yum install freetype* yum install urw-fonts diff --git a/docs/sources/installation/upgrading.md b/docs/sources/installation/upgrading.md index 4cd8471e441..6a4b4e8f047 100644 --- a/docs/sources/installation/upgrading.md +++ b/docs/sources/installation/upgrading.md @@ -29,7 +29,7 @@ installed grafana to custom location using a binary tar/zip it is usally in ` mysqldump -u root -p[root_password] [grafana] > grafana_backup.sql @@ -39,7 +39,7 @@ restore: #### postgres -``` +```bash backup: > pg_dump grafana > grafana_backup @@ -54,7 +54,7 @@ and execute the same `dpkg -i` command but with the new package. It will upgrade If you used our APT repository: -``` +```bash sudo apt-get update sudo apt-get install grafana ``` @@ -73,14 +73,14 @@ and execute the same `yum install` or `rpm -i` command but with the new package. If you used our YUM repository: -``` +```bash sudo yum update grafana ``` ### Docker This just an example, details depend on how you configured your grafana container. -``` +```bash docker pull grafana docker stop my-grafana-container docker rm my-grafana-container diff --git a/docs/sources/plugins/developing/code-styleguide.md b/docs/sources/plugins/developing/code-styleguide.md index 44379c4d4e5..9ee91412e24 100644 --- a/docs/sources/plugins/developing/code-styleguide.md +++ b/docs/sources/plugins/developing/code-styleguide.md @@ -23,7 +23,7 @@ The most important fields are the first three, especially the id. The convention Examples: -``` +```bash raintank-worldping-app grafana-simple-json-datasource grafana-piechart-panel @@ -66,7 +66,7 @@ The README.md file is rendered both on Grafana.net and in the plugins section in Here is a typical directory structure for a plugin. -``` +```bash johnnyb-awesome-datasource |-- dist |-- spec diff --git a/docs/sources/plugins/developing/datasources.md b/docs/sources/plugins/developing/datasources.md index 612a0786976..0149f06e1aa 100644 --- a/docs/sources/plugins/developing/datasources.md +++ b/docs/sources/plugins/developing/datasources.md @@ -45,7 +45,7 @@ The javascript object that communicates with the database and transforms data to The Datasource should contain the following functions: -``` +```javascript query(options) //used by panels to get data testDatasource() //used by datasource configuration page to make sure the connection is working annotationQuery(options) // used by dashboards to get annotations diff --git a/docs/sources/plugins/installation.md b/docs/sources/plugins/installation.md index 27f6f583d9a..526f1d5d4eb 100644 --- a/docs/sources/plugins/installation.md +++ b/docs/sources/plugins/installation.md @@ -30,37 +30,37 @@ On Linux systems the grafana-cli will assume that the grafana plugin directory i ### Grafana-cli Commands List available plugins -``` +```bash grafana-cli plugins list-remote ``` Install the latest version of a plugin -``` +```bash grafana-cli plugins install ``` Install a specific version of a plugin -``` +```bash grafana-cli plugins install ``` List installed plugins -``` +```bash grafana-cli plugins ls ``` Update all installed plugins -``` +```bash grafana-cli plugins update-all ``` Update one plugin -``` +```bash grafana-cli plugins update ``` Remove one plugin -``` +```bash grafana-cli plugins remove ``` @@ -73,7 +73,7 @@ The Download URL from Grafana.com API is in this form: `https://grafana.com/api/plugins//versions//download` You can specify a local URL by using the `--pluginUrl` option. -``` +```bash grafana-cli --pluginUrl https://nexus.company.com/grafana/plugins/-.zip plugins install ``` @@ -84,7 +84,7 @@ To manually install a Plugin via the Grafana.com API: {{< imgbox img="/img/docs/installation-tab.png" caption="Installation Tab" >}} 2. Use the Grafana API to find the plugin using this url `https://grafana.com/api/plugins/`. For example: https://grafana.com/api/plugins/jdbranham-diagram-panel should return: - ``` + ```bash { "id": 145, "typeId": 3, @@ -97,7 +97,7 @@ To manually install a Plugin via the Grafana.com API: ``` 3. Find the download link: - ``` + ```bash { "rel": "download", "href": "/plugins/jdbranham-diagram-panel/versions/1.4.0/download" diff --git a/docs/sources/project/building_from_source.md b/docs/sources/project/building_from_source.md index f8dcc9a949c..1e4f0421a0c 100644 --- a/docs/sources/project/building_from_source.md +++ b/docs/sources/project/building_from_source.md @@ -20,20 +20,20 @@ dev environment. Grafana ships with its own required backend server; also comple ## Get Code Create a directory for the project and set your path accordingly (or use the [default Go workspace directory](https://golang.org/doc/code.html#GOPATH)). Then download and install Grafana into your $GOPATH directory: -``` +```bash export GOPATH=`pwd` go get github.com/grafana/grafana ``` On Windows use setx instead of export and then restart your command prompt: -``` +```bash setx GOPATH %cd% ``` You may see an error such as: `package github.com/grafana/grafana: no buildable Go source files`. This is just a warning, and you can proceed with the directions. ## Building the backend -``` +```bash cd $GOPATH/src/github.com/grafana/grafana go run build.go setup go run build.go build # (or 'go build ./pkg/cmd/grafana-server') @@ -45,7 +45,7 @@ to install GCC. We recommend [TDM-GCC](http://tdm-gcc.tdragon.net/download). [node-gyp](https://github.com/nodejs/node-gyp#installation) is the Node.js native addon build tool and it requires extra dependencies to be installed on Windows. In a command prompt which is run as administrator, run: -``` +```bash npm --add-python-to-path='true' --debug install --global windows-build-tools ``` @@ -53,7 +53,7 @@ npm --add-python-to-path='true' --debug install --global windows-build-tools For this you need nodejs (v.6+). -``` +```bash npm install -g yarn yarn install --pure-lockfile npm run build @@ -62,7 +62,7 @@ npm run build ## Running Grafana Locally You can run a local instance of Grafana by running: -``` +```bash ./bin/grafana-server ``` If you built the binary with `go run build.go build`, run `./bin/grafana-server` @@ -76,7 +76,7 @@ Open grafana in your browser (default [http://localhost:3000](http://localhost:3 To add features, customize your config, etc, you'll need to rebuild the backend when you change the source code. We use a tool named `bra` that does this. -``` +```bash go get github.com/Unknwon/bra bra run @@ -88,7 +88,7 @@ You'll also need to run `npm run watch` to watch for changes to the front-end (t This step builds linux packages and requires that fpm is installed. Install fpm via `gem install fpm`. -``` +```bash go run build.go build package ``` diff --git a/docs/sources/reference/dashboard.md b/docs/sources/reference/dashboard.md index 3bbecfcef4d..d2da621a5a3 100644 --- a/docs/sources/reference/dashboard.md +++ b/docs/sources/reference/dashboard.md @@ -24,7 +24,7 @@ When a user creates a new dashboard, a new dashboard JSON object is initialized > Note: In the following JSON, id is shown as null which is the default value assigned to it until a dashboard is saved. Once a dashboard is saved, an integer value is assigned to the `id` field. -``` +```json { "id": null, "title": "New dashboard", diff --git a/docs/sources/reference/sharing.md b/docs/sources/reference/sharing.md index 61ae1f761eb..badd3b5712a 100644 --- a/docs/sources/reference/sharing.md +++ b/docs/sources/reference/sharing.md @@ -43,7 +43,7 @@ You also get a link to service side rendered PNG of the panel. Useful if you wan Example of a link to a server-side rendered PNG: -``` +```bash http://play.grafana.org/render/dashboard-solo/db/grafana-play-home?orgId=1&panelId=4&from=1499272191563&to=1499279391563&width=1000&height=500&tz=UTC%2B02%3A00&timeout=5000 ``` diff --git a/docs/sources/tutorials/api_org_token_howto.md b/docs/sources/tutorials/api_org_token_howto.md index e985b499dbe..984cfd40bd0 100644 --- a/docs/sources/tutorials/api_org_token_howto.md +++ b/docs/sources/tutorials/api_org_token_howto.md @@ -22,24 +22,24 @@ Some parts of the API are only available through basic authentication and these The task is to create a new organization and then add a Token that can be used by other users. In the examples below which use basic auth, the user is `admin` and the password is `admin`. 1. [Create the org](http://docs.grafana.org/http_api/org/#create-organisation). Here is an example using curl: - ``` + ```bash curl -X POST -H "Content-Type: application/json" -d '{"name":"apiorg"}' http://admin:admin@localhost:3000/api/orgs ``` This should return a response: `{"message":"Organization created","orgId":6}`. Use the orgId for the next steps. 2. Optional step. If the org was created previously and/or step 3 fails then first [add your Admin user to the org](http://docs.grafana.org/http_api/org/#add-user-in-organisation): - ``` + ```bash curl -X POST -H "Content-Type: application/json" -d '{"loginOrEmail":"admin", "role": "Admin"}' http://admin:admin@localhost:3000/api/orgs//users ``` 3. [Switch the org context for the Admin user to the new org](http://docs.grafana.org/http_api/user/#switch-user-context): - ``` + ```bash curl -X POST http://admin:admin@localhost:3000/api/user/using/ ``` 4. [Create the API token](http://docs.grafana.org/http_api/auth/#create-api-key): - ``` + ```bash curl -X POST -H "Content-Type: application/json" -d '{"name":"apikeycurl", "role": "Admin"}' http://admin:admin@localhost:3000/api/auth/keys ``` @@ -49,11 +49,11 @@ The task is to create a new organization and then add a Token that can be used b ## How To Add A Dashboard -Using the Token that was created in the previous step, you can create a dashboard or carry out other actions without having to switch organizations. +Using the Token that was created in the previous step, you can create a dashboard or carry out other actions without having to switch organizations. 1. [Add a dashboard](http://docs.grafana.org/http_api/dashboard/#create-update-dashboard) using the key (or bearer token as it is also called): - ``` + ```bash curl -X POST --insecure -H "Authorization: Bearer eyJrIjoiR0ZXZmt1UFc0OEpIOGN5RWdUalBJTllUTk83VlhtVGwiLCJuIjoiYXBpa2V5Y3VybCIsImlkIjo2fQ==" -H "Content-Type: application/json" -d '{ "dashboard": { "id": null, diff --git a/docs/sources/tutorials/hubot_howto.md b/docs/sources/tutorials/hubot_howto.md index 58c902951ee..2f122e5b4e2 100644 --- a/docs/sources/tutorials/hubot_howto.md +++ b/docs/sources/tutorials/hubot_howto.md @@ -39,9 +39,9 @@ read the official [Getting Started With Hubot](https://hubot.github.com/docs/) g ## Install Hubot-Grafana script In your Hubot project repo install the Grafana plugin using `npm`: - - npm install hubot-grafana --save - +```bash +npm install hubot-grafana --save +``` Edit the file external-scripts.json, and add hubot-grafana to the list of plugins. ```json @@ -56,13 +56,15 @@ Edit the file external-scripts.json, and add hubot-grafana to the list of plugin The `hubot-grafana` plugin requires a number of environment variables to be set in order to work properly. - export HUBOT_GRAFANA_HOST=http://play.grafana.org - export HUBOT_GRAFANA_API_KEY=abcd01234deadbeef01234 - export HUBOT_GRAFANA_S3_BUCKET=mybucket - export HUBOT_GRAFANA_S3_ACCESS_KEY_ID=ABCDEF123456XYZ - export HUBOT_GRAFANA_S3_SECRET_ACCESS_KEY=aBcD01234dEaDbEef01234 - export HUBOT_GRAFANA_S3_PREFIX=graphs - export HUBOT_GRAFANA_S3_REGION=us-standard +```bash +export HUBOT_GRAFANA_HOST=http://play.grafana.org +export HUBOT_GRAFANA_API_KEY=abcd01234deadbeef01234 +export HUBOT_GRAFANA_S3_BUCKET=mybucket +export HUBOT_GRAFANA_S3_ACCESS_KEY_ID=ABCDEF123456XYZ +export HUBOT_GRAFANA_S3_SECRET_ACCESS_KEY=aBcD01234dEaDbEef01234 +export HUBOT_GRAFANA_S3_PREFIX=graphs +export HUBOT_GRAFANA_S3_REGION=us-standard +``` ### Grafana server side rendering @@ -112,7 +114,9 @@ can create hubot command aliases with the hubot script `hubot-alias`. Install it: - npm i --save hubot-alias +```bash +npm i --save hubot-alias +``` Now add `hubot-alias` to the list of plugins in `external-scripts.json` and restart hubot. From e951200404e5657439237ceb2885de10001fc16f Mon Sep 17 00:00:00 2001 From: Patrick O'Carroll Date: Thu, 5 Oct 2017 19:01:15 +0200 Subject: [PATCH 66/83] docs page for authproxy (#9420) --- docs/sources/tutorials/authproxy.md | 243 ++++++++++++++++++++++++++++ 1 file changed, 243 insertions(+) create mode 100644 docs/sources/tutorials/authproxy.md diff --git a/docs/sources/tutorials/authproxy.md b/docs/sources/tutorials/authproxy.md new file mode 100644 index 00000000000..d4d2b9926fc --- /dev/null +++ b/docs/sources/tutorials/authproxy.md @@ -0,0 +1,243 @@ ++++ +title = "Grafana Authproxy" +type = "docs" +keywords = ["grafana", "tutorials", "authproxy"] +[menu.docs] +parent = "tutorials" +weight = 10 ++++ + +# Grafana Authproxy + +AuthProxy allows you to offload the authentication of users to a web server (there are many reasons why you’d want to run a web server in front of a production version of Grafana, especially if it’s exposed to the Internet). + +Popular web servers have a very extensive list of pluggable authentication modules, and any of them can be used with the AuthProxy feature. + +The Grafana AuthProxy feature is very simple in design, but it is this simplicity that makes it so powerful. + +## Interacting with Grafana’s AuthProxy via curl + +The AuthProxy feature can be configured through the Grafana configuration file with the following options: + +```js +[auth.proxy] +enabled = true +header_name = X-WEBAUTH-USER +header_property = username +auto_sign_up = true +``` + +* **enabled**: this is to toggle the feature on or off +* **header_name**: this is the HTTP header name that passes the username or email address of the authenticated user to Grafana. Grafana will trust what ever username is contained in this header and automatically log the user in. +* **header_property**: this tells Grafana whether the value in the header_name is a username or an email address. (In Grafana you can log in using your account username or account email) +* **auto_sign_up**: If set to true, Grafana will automatically create user accounts in the Grafana DB if one does not exist. If set to false, users who do not exist in the GrafanaDB won’t be able to log in, even though their username and password are valid. + +With a fresh install of Grafana, using the above configuration for the authProxy feature, we can send a simple API call to list all users. The only user that will be present is the default “Admin” user that is added the first time Grafana starts up. As you can see all we need to do to authenticate the request is to provide the “X-WEBAUTH-USER” header. + +```bash +curl -H "X-WEBAUTH-USER: admin" http://localhost:3000/api/users +[ + { + "id":1, + "name":"", + "login":"admin", + "email":"admin@localhost", + "isAdmin":true + } +] +``` + +We can then send a second request to the `/api/user` method which will return the details of the logged in user. We will use this request to show how Grafana automatically adds the new user we specify to the system. Here we create a new user called “anthony”. + +```bash +curl -H "X-WEBAUTH-USER: anthony" http://localhost:3000/api/user +{ + "email":"anthony", + "name":"", + "login":"anthony", + "theme":"", + "orgId":1, + "isGrafanaAdmin":false +} +``` + +## Making Apache’s auth work together with Grafana’s AuthProxy + +I’ll demonstrate how to use Apache for authenticating users. In this example we use BasicAuth with Apache’s text file based authentication handler, i.e. htpasswd files. However, any available Apache authentication capabilities could be used. + +### Apache BasicAuth + +In this example we use Apache as a reverseProxy in front of Grafana. Apache handles the Authentication of users before forwarding requests to the Grafana backend service. + +#### Apache configuration + +```bash + + ServerAdmin webmaster@authproxy + ServerName authproxy + ErrorLog "logs/authproxy-error_log" + CustomLog "logs/authproxy-access_log" common + + + AuthType Basic + AuthName GrafanaAuthProxy + AuthBasicProvider file + AuthUserFile /etc/apache2/grafana_htpasswd + Require valid-user + + RewriteEngine On + RewriteRule .* - [E=PROXY_USER:%{LA-U:REMOTE_USER},NS] + RequestHeader set X-WEBAUTH-USER "%{PROXY_USER}e" + + + RequestHeader unset Authorization + + ProxyRequests Off + ProxyPass / http://localhost:3000/ + ProxyPassReverse / http://localhost:3000/ + +``` + +* The first 4 lines of the virtualhost configuration are standard, so we won’t go into detail on what they do. + +* We use a **\** configuration block for applying our authentication rules to every proxied request. These rules include requiring basic authentication where user:password credentials are stored in the **/etc/apache2/grafana_htpasswd** file. This file can be created with the `htpasswd` command. + + * The next part of the configuration is the tricky part. We use Apache’s rewrite engine to create our **X-WEBAUTH-USER header**, populated with the authenticated user. + + * **RewriteRule .* - [E=PROXY_USER:%{LA-U:REMOTE_USER}, NS]**: This line is a little bit of magic. What it does, is for every request use the rewriteEngines look-ahead (LA-U) feature to determine what the REMOTE_USER variable would be set to after processing the request. Then assign the result to the variable PROXY_USER. This is neccessary as the REMOTE_USER variable is not available to the RequestHeader function. + + * **RequestHeader set X-WEBAUTH-USER “%{PROXY_USER}e”**: With the authenticated username now stored in the PROXY_USER variable, we create a new HTTP request header that will be sent to our backend Grafana containing the username. + +* The **RequestHeader unset Authorization** removes the Authorization header from the HTTP request before it is forwarded to Grafana. This ensures that Grafana does not try to authenticate the user using these credentials (BasicAuth is a supported authentication handler in Grafana). + +* The last 3 lines are then just standard reverse proxy configuration to direct all authenticated requests to our Grafana server running on port 3000. + +#### Grafana configuration + +```bash +############# Users ################ +[users] + # disable user signup / registration +allow_sign_up = false + +# Set to true to automatically assign new users to the default organization (id 1) +auto_assign_org = true + +# Default role new users will be automatically assigned (if auto_assign_org above is set to true) + auto_assign_org_role = Editor + + +############ Auth Proxy ######## +[auth.proxy] +enabled = true + +# the Header name that contains the authenticated user. +header_name = X-WEBAUTH-USER + +# does the user authenticate against the proxy using a 'username' or an 'email' +header_property = username + +# automatically add the user to the system if they don't already exist. +auto_sign_up = true +``` + +#### Full walk through using Docker. + +##### Grafana Container + +For this example, we use the offical Grafana docker image available at [Docker Hub](https://hub.docker.com/r/grafana/grafana/) + +* Create a file `grafana.ini` with the following contents + +```bash +[users] +allow_sign_up = false +auto_assign_org = true +auto_assign_org_role = Editor + +[auth.proxy] +enabled = true +header_name = X-WEBAUTH-USER +header_property = username +auto_sign_up = true +``` + +* Launch the Grafana container, using our custom grafana.ini to replace `/etc/grafana/grafana.ini`. We dont expose any ports for this container as it will only be connected to by our Apache container. + +```bash +docker run -i -v $(pwd)/grafana.ini:/etc/grafana/grafana.ini --name grafana grafana/grafana +``` + +### Apache Container + +For this example we use the offical Apache docker image available at [Docker Hub](https://hub.docker.com/_/httpd/) + +* Create a file `httpd.conf` with the following contents + +```bash +ServerRoot "/usr/local/apache2" +Listen 80 +LoadModule authn_file_module modules/mod_authn_file.so +LoadModule authn_core_module modules/mod_authn_core.so +LoadModule authz_host_module modules/mod_authz_host.so +LoadModule authz_user_module modules/mod_authz_user.so +LoadModule authz_core_module modules/mod_authz_core.so +LoadModule auth_basic_module modules/mod_auth_basic.so +LoadModule log_config_module modules/mod_log_config.so +LoadModule env_module modules/mod_env.so +LoadModule headers_module modules/mod_headers.so +LoadModule unixd_module modules/mod_unixd.so +LoadModule rewrite_module modules/mod_rewrite.so +LoadModule proxy_module modules/mod_proxy.so +LoadModule proxy_http_module modules/mod_proxy_http.so + +User daemon +Group daemon + +ServerAdmin you@example.com + + AllowOverride none + Require all denied + +DocumentRoot "/usr/local/apache2/htdocs" +ErrorLog /proc/self/fd/2 +LogLevel error + + LogFormat "%h %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-Agent}i\"" combined + LogFormat "%h %l %u %t \"%r\" %>s %b" common + + LogFormat "%h %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-Agent}i\" %I %O" combinedio + + CustomLog /proc/self/fd/1 common + + + AuthType Basic + AuthName GrafanaAuthProxy + AuthBasicProvider file + AuthUserFile /tmp/htpasswd + Require valid-user + RewriteEngine On + RewriteRule .* - [E=PROXY_USER:%{LA-U:REMOTE_USER},NS] + RequestHeader set X-WEBAUTH-USER "%{PROXY_USER}e" + +RequestHeader unset Authorization +ProxyRequests Off +ProxyPass / http://grafana:3000/ +ProxyPassReverse / http://grafana:3000/ +``` + +* Create a htpasswd file. We create a new user **anthony** with the password **password** + + ```bash + htpasswd -bc htpasswd anthony password + ``` + +* Launch the httpd container using our custom httpd.conf and our htpasswd file. The container will listen on port 80, and we create a link to the **grafana** container so that this container can resolve the hostname **grafana** to the grafana container’s ip address. + + ```bash + docker run -i -p 80:80 --link grafana:grafana -v $(pwd)/httpd.conf:/usr/local/apache2/conf/httpd.conf -v $(pwd)/htpasswd:/tmp/htpasswd httpd:2.4 + ``` + +### Use grafana. + +With our Grafana and Apache containers running, you can now connect to http://localhost/ and log in using the username/password we created in the htpasswd file. \ No newline at end of file From d6b512478e1af843abdf3a5c65bdc562fb525176 Mon Sep 17 00:00:00 2001 From: Patrick O'Carroll Date: Thu, 5 Oct 2017 19:01:23 +0200 Subject: [PATCH 67/83] changed jsontree to use jsonexplorer (#9416) --- .../app/core/components/jsontree/jsontree.ts | 197 +----------------- 1 file changed, 9 insertions(+), 188 deletions(-) diff --git a/public/app/core/components/jsontree/jsontree.ts b/public/app/core/components/jsontree/jsontree.ts index 5c39e2ab53c..d9972f907ac 100644 --- a/public/app/core/components/jsontree/jsontree.ts +++ b/public/app/core/components/jsontree/jsontree.ts @@ -1,201 +1,22 @@ - -/** Created by: Alex Wendland (me@alexwendland.com), 2014-08-06 - * - * angular-json-tree - * - * Directive for creating a tree-view out of a JS Object. Only loads - * sub-nodes on demand in order to improve performance of rendering large - * objects. - * - * Attributes: - * - object (Object, 2-way): JS object to build the tree from - * - start-expanded (Boolean, 1-way, ?=true): should the tree default to expanded - * - * Usage: - * // In the controller - * scope.someObject = { - * test: 'hello', - * array: [1,1,2,3,5,8] - * }; - * // In the html - * - * - * Dependencies: - * - utils (json-tree.js) - * - ajsRecursiveDirectiveHelper (json-tree.js) - * - * Test: json-tree-test.js - */ - -import angular from 'angular'; import coreModule from 'app/core/core_module'; - -var utils = { - /* See link for possible type values to check against. - * http://stackoverflow.com/questions/4622952/json-object-containing-array - * - * Value Class Type - * ------------------------------------- - * "foo" String string - * new String("foo") String object - * 1.2 Number number - * new Number(1.2) Number object - * true Boolean boolean - * new Boolean(true) Boolean object - * new Date() Date object - * new Error() Error object - * [1,2,3] Array object - * new Array(1, 2, 3) Array object - * new Function("") Function function - * /abc/g RegExp object (function in Nitro/V8) - * new RegExp("meow") RegExp object (function in Nitro/V8) - * {} Object object - * new Object() Object object - */ - is: function is(obj, clazz) { - return Object.prototype.toString.call(obj).slice(8, -1) === clazz; - }, - - // See above for possible values - whatClass: function whatClass(obj) { - return Object.prototype.toString.call(obj).slice(8, -1); - }, - - // Iterate over an objects keyset - forKeys: function forKeys(obj, f) { - for (var key in obj) { - if (obj.hasOwnProperty(key) && typeof obj[key] !== 'function') { - if (f(key, obj[key])) { - break; - } - } - } - } -}; +import {JsonExplorer} from '../json_explorer/json_explorer'; coreModule.directive('jsonTree', [function jsonTreeDirective() { - return { + return{ restrict: 'E', scope: { object: '=', startExpanded: '@', rootName: '@', }, - template: '' - }; -}]); + link: function(scope, elem) { -coreModule.directive('jsonNode', ['ajsRecursiveDirectiveHelper', function jsonNodeDirective(ajsRecursiveDirectiveHelper) { - return { - restrict: 'E', - scope: { - key: '=', - value: '=', - startExpanded: '@' - }, - compile: function jsonNodeDirectiveCompile(elem) { - return ajsRecursiveDirectiveHelper.compile(elem, this); - }, - template: ' {{key}}' + - ' {{value}}' + - ' ' + - ' {{preview}}' + - '
        ' + - '
      • ' + - ' ' + - '
      • ' + - '
      ', - pre: function jsonNodeDirectiveLink(scope, elem, attrs) { - // Set value's type as Class for CSS styling - elem.addClass(utils.whatClass(scope.value).toLowerCase()); - // If the value is an Array or Object, use expandable view type - if (utils.is(scope.value, 'Object') || utils.is(scope.value, 'Array')) { - scope.isExpandable = true; - // Add expandable class for CSS usage - elem.addClass('expandable'); - // Setup preview text - var isArray = utils.is(scope.value, 'Array'); - scope.preview = isArray ? '[ ' : '{ '; - utils.forKeys(scope.value, function jsonNodeDirectiveLinkForKeys(key, value) { - if (value === null) { scope.value[key] = 'null'; } - if (isArray) { - scope.preview += value + ', '; - } else { - scope.preview += key + ': ' + value + ', '; - } - }); - scope.preview = scope.preview.substring(0, scope.preview.length - (scope.preview.length > 2 ? 2 : 0)) + (isArray ? ' ]' : ' }'); - // If directive initially has isExpanded set, also set shouldRender to true - if (scope.startExpanded) { - scope.shouldRender = true; - elem.addClass('expanded'); - } - // Setup isExpanded state handling - scope.isExpanded = scope.startExpanded; - scope.toggleExpanded = function jsonNodeDirectiveToggleExpanded() { - scope.isExpanded = !scope.isExpanded; - if (scope.isExpanded) { - elem.addClass('expanded'); - } else { - elem.removeClass('expanded'); - } - // For delaying subnode render until requested - scope.shouldRender = true; - }; - } else { - scope.isExpandable = false; - // Add expandable class for CSS usage - elem.addClass('not-expandable'); - } - } - }; -}]); - -/** Added by: Alex Wendland (me@alexwendland.com), 2014-08-09 - * Source: http://stackoverflow.com/questions/14430655/recursion-in-angular-directives - * - * Used to allow for recursion within directives - */ -coreModule.factory('ajsRecursiveDirectiveHelper', ['$compile', function RecursiveDirectiveHelper($compile) { - return { - /** - * Manually compiles the element, fixing the recursion loop. - * @param element - * @param [link] A post-link function, or an object with function(s) registered via pre and post properties. - * @returns An object containing the linking functions. - */ - compile: function RecursiveDirectiveHelperCompile(element, link) { - // Normalize the link parameter - if (angular.isFunction(link)) { - link = { - post: link - }; - } - - // Break the recursion loop by removing the contents - var contents = element.contents().remove(); - var compiledContents; - return { - pre: (link && link.pre) ? link.pre : null, - /** - * Compiles and re-adds the contents - */ - post: function RecursiveDirectiveHelperCompilePost(scope, element) { - // Compile the contents - if (!compiledContents) { - compiledContents = $compile(contents); - } - // Re-add the compiled contents to the element - compiledContents(scope, function (clone) { - element.append(clone); - }); - - // Call the post-linking function, if any - if (link && link.post) { - link.post.apply(null, arguments); - } - } - }; + var jsonExp = new JsonExplorer(scope.object, 3, { + animateOpen: true + }) + + const html = jsonExp.render(true); + elem.html(html); } }; }]); From e42c118b3d13443c5c201e9415ce5fcd18c05ad3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Thu, 5 Oct 2017 19:06:46 +0200 Subject: [PATCH 68/83] fix: missing semicolon --- public/app/core/components/jsontree/jsontree.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/app/core/components/jsontree/jsontree.ts b/public/app/core/components/jsontree/jsontree.ts index d9972f907ac..52fb64e1c87 100644 --- a/public/app/core/components/jsontree/jsontree.ts +++ b/public/app/core/components/jsontree/jsontree.ts @@ -13,7 +13,7 @@ coreModule.directive('jsonTree', [function jsonTreeDirective() { var jsonExp = new JsonExplorer(scope.object, 3, { animateOpen: true - }) + }); const html = jsonExp.render(true); elem.html(html); From 213ba0377b2bbf61093da70ad38e66deae413396 Mon Sep 17 00:00:00 2001 From: Adrien Raffin-Caboisse Date: Thu, 5 Oct 2017 20:26:28 +0200 Subject: [PATCH 69/83] Fix coloring in singlestat if null value (#9438) This modification aim to allow users to set value via textMapping and these values to be used in background coloring as it text coloring. This pull request closes #8404, but doesn't agree with #9012. The issue #9012 consider that no coloring output should be put when there is no data. I partially agree with this as I explicitely setted a value in the textMapping I obviously want to treat `N/A` as a number. `data.valueFormatted` contain the stringified version of `data.value` If `Number()` cannot convert a string into a number a `NaN` value is returned. So the code is still valid if the inputted value in `data.valueFormatted` is not a number. --- public/app/plugins/panel/singlestat/module.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/public/app/plugins/panel/singlestat/module.ts b/public/app/plugins/panel/singlestat/module.ts index b4ee2bf7fb7..f9cf5ed2955 100644 --- a/public/app/plugins/panel/singlestat/module.ts +++ b/public/app/plugins/panel/singlestat/module.ts @@ -589,8 +589,8 @@ class SingleStatCtrl extends MetricsPanelCtrl { var body = panel.gauge.show ? '' : getBigValueHtml(); - if (panel.colorBackground && !isNaN(data.value)) { - var color = getColorForValue(data, data.value); + if (panel.colorBackground && !isNaN(Number(data.valueFormatted))) { + var color = getColorForValue(data, Number(data.valueFormatted)); if (color) { $panelContainer.css('background-color', color); if (scope.fullscreen) { From 7af4daf23b5678e931a21f0e34c3896f5a63e03e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Thu, 5 Oct 2017 20:29:06 +0200 Subject: [PATCH 70/83] Revert "Fix coloring in singlestat if null value (#9438)" (#9443) This reverts commit 213ba0377b2bbf61093da70ad38e66deae413396. --- public/app/plugins/panel/singlestat/module.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/public/app/plugins/panel/singlestat/module.ts b/public/app/plugins/panel/singlestat/module.ts index f9cf5ed2955..b4ee2bf7fb7 100644 --- a/public/app/plugins/panel/singlestat/module.ts +++ b/public/app/plugins/panel/singlestat/module.ts @@ -589,8 +589,8 @@ class SingleStatCtrl extends MetricsPanelCtrl { var body = panel.gauge.show ? '' : getBigValueHtml(); - if (panel.colorBackground && !isNaN(Number(data.valueFormatted))) { - var color = getColorForValue(data, Number(data.valueFormatted)); + if (panel.colorBackground && !isNaN(data.value)) { + var color = getColorForValue(data, data.value); if (color) { $panelContainer.css('background-color', color); if (scope.fullscreen) { From 5561a099b09ee467279218ccf0ebb8686197d120 Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Fri, 6 Oct 2017 14:41:39 +0300 Subject: [PATCH 71/83] singlestat: fix sizing bug #9337 (#9448) --- public/app/plugins/panel/singlestat/module.ts | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/public/app/plugins/panel/singlestat/module.ts b/public/app/plugins/panel/singlestat/module.ts index b4ee2bf7fb7..e6e8858d6e8 100644 --- a/public/app/plugins/panel/singlestat/module.ts +++ b/public/app/plugins/panel/singlestat/module.ts @@ -432,7 +432,8 @@ class SingleStatCtrl extends MetricsPanelCtrl { function addGauge() { var width = elem.width(); var height = elem.height(); - var dimension = Math.min(width, height); + // Allow to use a bit more space for wide gauges + var dimension = Math.min(width, height * 1.3); ctrl.invalidGaugeRange = false; if (panel.gauge.minValue > panel.gauge.maxValue) { @@ -469,8 +470,11 @@ class SingleStatCtrl extends MetricsPanelCtrl { var fontScale = parseInt(panel.valueFontSize) / 100; var fontSize = Math.min(dimension/5, 100) * fontScale; - var gaugeWidth = Math.min(dimension/6, 60); + // Reduce gauge width if threshold labels enabled + var gaugeWidthReduceRatio = panel.gauge.thresholdLabels ? 1.5 : 1; + var gaugeWidth = Math.min(dimension/6, 60) / gaugeWidthReduceRatio; var thresholdMarkersWidth = gaugeWidth/5; + var thresholdLabelFontSize = fontSize / 2.5; var options = { series: { @@ -491,8 +495,8 @@ class SingleStatCtrl extends MetricsPanelCtrl { values: thresholds, label: { show: panel.gauge.thresholdLabels, - margin: 8, - font: { size: 18 } + margin: thresholdMarkersWidth + 1, + font: { size: thresholdLabelFontSize } }, show: panel.gauge.thresholdMarkers, width: thresholdMarkersWidth, From 43903d71eca192cfffac0cfeb6e0f33357643d8d Mon Sep 17 00:00:00 2001 From: Lucas Costa Date: Fri, 6 Oct 2017 12:05:26 -0300 Subject: [PATCH 72/83] fix: threshold's colors in table panels (#9445) (#9453) --- public/app/plugins/panel/table/column_options.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/app/plugins/panel/table/column_options.html b/public/app/plugins/panel/table/column_options.html index b729a289970..3bd8ec24841 100644 --- a/public/app/plugins/panel/table/column_options.html +++ b/public/app/plugins/panel/table/column_options.html @@ -69,7 +69,7 @@
      Thresholds
      - +
      From 25aa9df2704ae23f808452a2280ae3dd13c17a0a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Sat, 7 Oct 2017 10:31:39 +0200 Subject: [PATCH 73/83] Create annotations (#8197) * annotations: add 25px space for events section * annotations: restored create annotation action * annotations: able to use fa icons as event markers * annotations: initial emoji support from twemoji lib * annotations: adjust fa icon position * annotations: initial emoji picker * annotation: include user info into annotation requests * annotation: add icon info * annotation: display user info in tooltip * annotation: fix region saving * annotation: initial region markers * annotation: fix region clearing (add flot-temp-elem class) * annotation: adjust styles a bit * annotations: minor fixes * annoations: removed userId look in loop, need a sql join or a user cache for this * annotation: fix invisible events * lib: changed twitter emoij lib to be npm dependency * annotation: add icon picker to Add Annotation dialog * annotation: save icon to annotation table * annotation: able to set custom icon for annotation added by user * annotations: fix emoji after library upgrade (switch to 72px) * emoji: temporary remove bad code points * annotations: improve icon picker * annotations: icon show icon picker at the top * annotations: use svg for emoji * annotations: fix region drawing when add annotation editor opened * annotations: use flot lib for drawing region fill * annotations: move regions building into event_manager * annotations: don't draw additional space if no events are got * annotations: deduplicate events * annotations: properly render cut regions * annotations: fix cut region building * annotations: refactor * annotations: adjust event section size * add-annotations: fix undefined default icon * create-annotations: edit event (frontend part) * fixed bug causes error when hover event marker * create-annotations: update event (backend) * ignore grafana-server debug binary in git (created VS Code) * create-annotations: use PUT request for updating annotation. * create-annotations: fixed time format when editing existing event * create-annotations: support for region update * create-annotations: fix bug with limit and event type * create-annotations: delete annotation * create-annotations: show only selected icon in edit mode * create-annotations: show event editor only for users with at least Editor role * create-annotations: handle double-sized emoji codepoints * create-annotations: refactor use CP_SEPARATOR from emojiDef * create-annotations: update emoji list, add categories. * create-annotations: copy SVG emoji into public/vendor/npm and use it as a base path * create-annotations: initial tabs for emoji picker * emoji-picker: adjust styles * emoji-picker: minor refactor * emoji-picker: refactor - rename and move into one directory * emoji-picker: build emoji elements on app load, not on picker open * emoji-picker: fix emoji searching * emoji-picker: refactor * emoji-picker: capitalize category name * emoji-picker: refactor move buildEmojiElem() into emoji_converter.ts for future reuse. * jquery.flot.events: refactor use buildEmojiElem() for making emojis, remove unused code for font awesome based icons. * emoji_converter: handle converting error * tech: updated * merged with master * shore: clean up some stuff * annotation: wip tags * annotation: filtering by tags * tags: parse out spaces etc. from a tags string * annotations: use tagsinput component for tag filtering * annotation: wip work on how we query alert & panel annotations * annotations: support for updating tags in an annotation * linting * annotations: work on unifying how alert history annotations and manual panel annotations are created * tslint: fixes * tags: create tag on blur as well Currently, the tags directive only creates the tag when the user presses enter. This change means the tag is created on blur as well (when the user clicks outside the input field). * annotations: fix update after refactoring * annotations: progress on how alert annotations are fetched * annotations: minor progress * annotations: progress * annotation: minor progress * annotations: move tag parsing from tooltip to ds Instead of parsing a tag string into an array in the annotation_tooltip class, this moves the parsing to the datasources. InfluxDB ds already does that parsing. Graphite now has it. * annotations: more work on querying * annotations: change from tags as string to array when saving in the db and in the api. * annotations: delete tag link if removed on edit * annotation: more work on depricating annotation title * annotations: delete tag links on delete * annotations: fix for find * annotation: added user to annotation tooltip and added alertName to annoation dto * annotations: use id from route instead from cmd for updating * annotations: http api docs * create annotation: last edits * annotations: minor fix for querying annotations before dashboard saved * annotations: fix for popover placement when legend is on the side (and doubel render pass is causing original marker to be removed) * annotations: changing how the built in query gets added * annotation: added time to header in edit mode * tests: fixed jshint built issue --- .gitignore | 1 + docs/sources/http_api/annotations.md | 189 ++++++++++++++ package.json | 5 +- pkg/api/annotations.go | 103 ++++++-- pkg/api/api.go | 3 + pkg/api/avatar/avatar.go | 4 +- pkg/api/dtos/annotations.go | 49 ++-- pkg/models/tags.go | 60 +++++ pkg/models/tags_test.go | 95 +++++++ pkg/services/alerting/result_handler.go | 4 +- pkg/services/annotations/annotations.go | 66 +++-- pkg/services/sqlstore/annotation.go | 181 +++++++++++-- pkg/services/sqlstore/annotation_test.go | 208 +++++++++++++++ pkg/services/sqlstore/dashboard.go | 1 + .../sqlstore/migrations/annotation_mig.go | 33 +++ .../sqlstore/migrations/migrations.go | 1 + pkg/services/sqlstore/migrations/tag_mig.go | 24 ++ .../app/core/components/dashboard_selector.ts | 3 - public/app/core/components/grafana_app.ts | 6 + public/app/core/components/info_popover.ts | 2 + public/app/core/directives/tags.js | 1 + public/app/core/nav_model_srv.ts | 2 +- public/app/features/alerting/alert_def.ts | 1 - .../annotations/annotation_tooltip.ts | 47 ++-- .../features/annotations/annotations_srv.ts | 242 ++++++++++++------ .../app/features/annotations/editor_ctrl.ts | 21 +- public/app/features/annotations/event.ts | 4 +- .../app/features/annotations/event_editor.ts | 54 +++- .../app/features/annotations/event_manager.ts | 143 ++++++++--- .../features/annotations/partials/editor.html | 89 ++++--- .../annotations/partials/event_editor.html | 67 +++-- .../specs/annotations_srv_specs.ts | 40 +++ public/app/features/dashboard/model.ts | 25 ++ .../dashboard/specs/dashboard_model_specs.ts | 31 +-- .../dashboard/specs/exporter_specs.ts | 6 +- .../features/dashboard/submenu/submenu.html | 2 +- public/app/features/org/prefs_control.ts | 10 +- public/app/headers/common.d.ts | 6 - .../datasource/elasticsearch/datasource.ts | 23 +- .../datasource/elasticsearch/index_pattern.ts | 2 +- .../partials/annotations.editor.html | 26 +- .../datasource/elasticsearch/query_builder.ts | 2 +- .../datasource/elasticsearch/query_def.ts | 2 +- .../plugins/datasource/grafana/datasource.ts | 70 +++-- .../app/plugins/datasource/grafana/module.ts | 11 +- .../grafana/partials/annotations.editor.html | 25 +- .../plugins/datasource/graphite/datasource.ts | 23 +- .../graphite/specs/datasource_specs.ts | 106 ++++++-- .../plugins/datasource/influxdb/datasource.ts | 2 - .../influxdb/partials/annotations.editor.html | 12 +- public/app/plugins/datasource/mysql/module.ts | 1 - .../datasource/mysql/response_parser.ts | 4 +- .../mysql/specs/datasource_specs.ts | 11 +- .../plugins/datasource/opentsdb/datasource.js | 3 +- .../app/plugins/panel/alertlist/module.html | 2 +- public/app/plugins/panel/graph/graph.ts | 21 +- .../plugins/panel/graph/jquery.flot.events.js | 225 +++++++++++++++- public/app/system.conf.js | 83 ++++++ public/sass/_grafana.scss | 1 + public/sass/_variables.dark.scss | 3 +- public/sass/_variables.light.scss | 6 +- public/sass/components/_drop.scss | 7 + public/sass/components/_icon-picker.scss | 26 ++ public/sass/components/_panel_graph.scss | 33 ++- public/sass/mixins/_drop_element.scss | 4 - public/test/test-main.js | 130 ++++++++++ public/vendor/flot/jquery.flot.js | 7 +- .../vendor/tagsinput/bootstrap-tagsinput.js | 12 +- scripts/webpack/webpack.common.js | 9 +- tasks/options/copy.js | 45 ++++ tsconfig.json | 6 +- tslint.json | 3 +- 72 files changed, 2240 insertions(+), 535 deletions(-) create mode 100644 docs/sources/http_api/annotations.md create mode 100644 pkg/models/tags.go create mode 100644 pkg/models/tags_test.go create mode 100644 pkg/services/sqlstore/annotation_test.go create mode 100644 pkg/services/sqlstore/migrations/tag_mig.go create mode 100644 public/app/features/annotations/specs/annotations_srv_specs.ts create mode 100644 public/app/system.conf.js create mode 100644 public/sass/components/_icon-picker.scss create mode 100644 public/test/test-main.js create mode 100644 tasks/options/copy.js diff --git a/.gitignore b/.gitignore index 03178388a7c..2113bb2920b 100644 --- a/.gitignore +++ b/.gitignore @@ -41,6 +41,7 @@ profile.cov .notouch /pkg/cmd/grafana-cli/grafana-cli /pkg/cmd/grafana-server/grafana-server +/pkg/cmd/grafana-server/debug /examples/*/dist /packaging/**/*.rpm /packaging/**/*.deb diff --git a/docs/sources/http_api/annotations.md b/docs/sources/http_api/annotations.md new file mode 100644 index 00000000000..2f148e9aded --- /dev/null +++ b/docs/sources/http_api/annotations.md @@ -0,0 +1,189 @@ ++++ +title = "Annotations HTTP API " +description = "Grafana Annotations HTTP API" +keywords = ["grafana", "http", "documentation", "api", "annotation", "annotations", "comment"] +aliases = ["/http_api/annotations/"] +type = "docs" +[menu.docs] +name = "Annotations" +identifier = "annotationshttp" +parent = "http_api" ++++ + +# Annotations resources / actions + +This is the API documentation for the new Grafana Annotations feature released in Grafana 4.6. Annotations are saved in the Grafana database (sqlite, mysql or postgres). Annotations can be global annotations that can be shown on any dashboard by configuring an annotation data source - they are filtered by tags. Or they can be tied to a panel on a dashboard and are then only shown on that panel. + +## Find Annotations + +`GET /api/annotations?from=1506676478816&to=1507281278816&tags=tag1&tags=tag2&limit=100` + +**Example Request**: + +```http +GET /api/annotations?from=1506676478816&to=1507281278816&tags=tag1&tags=tag2&limit=100 HTTP/1.1 +Accept: application/json +Content-Type: application/json +Authorization: Basic YWRtaW46YWRtaW4= +``` + + +Query Parameters: + +- `from`: epoch datetime in milliseconds. Optional. +- `to`: epoch datetime in milliseconds. Optional. +- `limit`: number. Optional - default is 10. Max limit for results returned. +- `alertId`: number. Optional. Find annotations for a specified alert. +- `dashboardId`: number. Optional. Find annotations that are scoped to a specific dashboard +- `panelId`: number. Optional. Find annotations that are scoped to a specific panel +- `tags`: string. Optional. Use this to filter global annotations. Global annotations are annotations from an annotation data source that are not connected specifically to a dashboard or panel. To do an "AND" filtering with multiple tags, specify the tags parameter multiple times e.g. `tags=tag1&tags=tag2`. + +**Example Response**: + +```http +HTTP/1.1 200 +Content-Type: application/json +[ + { + "id": 1124, + "alertId": 0, + "dashboardId": 468, + "panelId": 2, + "userId": 1, + "userName": "", + "newState": "", + "prevState": "", + "time": 1507266395000, + "text": "test", + "metric": "", + "regionId": 1123, + "type": "event", + "tags": [ + "tag1", + "tag2" + ], + "data": {} + }, + { + "id": 1123, + "alertId": 0, + "dashboardId": 468, + "panelId": 2, + "userId": 1, + "userName": "", + "newState": "", + "prevState": "", + "time": 1507265111000, + "text": "test", + "metric": "", + "regionId": 1123, + "type": "event", + "tags": [ + "tag1", + "tag2" + ], + "data": {} + } +] +``` + +## Create Annotation + +Creates an annotation in the Grafana database. The `dashboardId` and `panelId` fields are optional. If they are not specified then a global annotation is created and can be queried in any dashboard that adds the Grafana annotations data source. + +`POST /api/annotations` + +**Example Request**: + +```json +POST /api/annotations HTTP/1.1 +Accept: application/json +Content-Type: application/json + +{ + "dashboardId":468, + "panelId":1, + "time":1507037197339, + "isRegion":true, + "timeEnd":1507180805056, + "tags":["tag1","tag2"], + "text":"Annotation Description" +} +``` + +**Example Response**: + +```json +HTTP/1.1 200 +Content-Type: application/json + +{"message":"Annotation added"} +``` + +## Update Annotation + +`PUT /api/annotations/:id` + +**Example Request**: + +```json +PUT /api/annotations/1141 HTTP/1.1 +Accept: application/json +Content-Type: application/json + +{ + "time":1507037197339, + "isRegion":true, + "timeEnd":1507180805056, + "text":"Annotation Description", + "tags":["tag3","tag4","tag5"] +} +``` + +## Delete Annotation By Id + +`DELETE /api/annotation/:id` + +Deletes the annotation that matches the specified id. + +**Example Request**: + +```http +DELETE /api/annotation/1 HTTP/1.1 +Accept: application/json +Content-Type: application/json +Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk +``` + +**Example Response**: + +```http +HTTP/1.1 200 +Content-Type: application/json + +{"message":"Annotation deleted"} +``` + +## Delete Annotation By RegionId + +`DELETE /api/annotation/region/:id` + +Deletes the annotation that matches the specified region id. A region is an annotation that covers a timerange and has a start and end time. In the Grafana database, this is a stored as two annotations connected by a region id. + +**Example Request**: + +```http +DELETE /api/annotation/region/1 HTTP/1.1 +Accept: application/json +Content-Type: application/json +Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk +``` + +**Example Response**: + +```http +HTTP/1.1 200 +Content-Type: application/json + +{"message":"Annotation region deleted"} +``` diff --git a/package.json b/package.json index 83025a3e8fc..1d8cc8bf64c 100644 --- a/package.json +++ b/package.json @@ -87,8 +87,8 @@ "tslint-loader": "^3.5.3", "typescript": "^2.5.2", "webpack": "^3.6.0", - "webpack-bundle-analyzer": "^2.9.0", "webpack-cleanup-plugin": "^0.5.1", + "webpack-bundle-analyzer": "^2.9.0", "webpack-merge": "^4.1.0", "zone.js": "^0.7.2" }, @@ -97,13 +97,14 @@ "watch": "./node_modules/.bin/webpack --progress --colors --watch --config scripts/webpack/webpack.dev.js", "build": "./node_modules/.bin/grunt build", "test": "./node_modules/.bin/grunt test", - "lint" : "./node_modules/.bin/tslint -c tslint.json --project ./tsconfig.json --type-check", + "lint" : "./node_modules/.bin/tslint -c tslint.json --project tsconfig.json --type-check", "watch-test": "./node_modules/grunt-cli/bin/grunt karma:dev" }, "license": "Apache-2.0", "dependencies": { "angular": "^1.6.6", "angular-bindonce": "^0.3.1", + "angular-mocks": "^1.6.6", "angular-native-dragdrop": "^1.2.2", "angular-route": "^1.6.6", "angular-sanitize": "^1.6.6", diff --git a/pkg/api/annotations.go b/pkg/api/annotations.go index e07c77f1c1d..300fa7f2cdc 100644 --- a/pkg/api/annotations.go +++ b/pkg/api/annotations.go @@ -2,6 +2,7 @@ package api import ( "github.com/grafana/grafana/pkg/api/dtos" + "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/middleware" "github.com/grafana/grafana/pkg/services/annotations" ) @@ -11,13 +12,12 @@ func GetAnnotations(c *middleware.Context) Response { query := &annotations.ItemQuery{ From: c.QueryInt64("from") / 1000, To: c.QueryInt64("to") / 1000, - Type: annotations.ItemType(c.Query("type")), OrgId: c.OrgId, AlertId: c.QueryInt64("alertId"), DashboardId: c.QueryInt64("dashboardId"), PanelId: c.QueryInt64("panelId"), Limit: c.QueryInt64("limit"), - NewState: c.QueryStrings("newState"), + Tags: c.QueryStrings("tags"), } repo := annotations.GetRepository() @@ -27,25 +27,14 @@ func GetAnnotations(c *middleware.Context) Response { return ApiError(500, "Failed to get annotations", err) } - result := make([]dtos.Annotation, 0) - for _, item := range items { - result = append(result, dtos.Annotation{ - AlertId: item.AlertId, - Time: item.Epoch * 1000, - Data: item.Data, - NewState: item.NewState, - PrevState: item.PrevState, - Text: item.Text, - Metric: item.Metric, - Title: item.Title, - PanelId: item.PanelId, - RegionId: item.RegionId, - Type: string(item.Type), - }) + if item.Email != "" { + item.AvatarUrl = dtos.GetGravatarUrl(item.Email) + } + item.Time = item.Time * 1000 } - return Json(200, result) + return Json(200, items) } func PostAnnotation(c *middleware.Context, cmd dtos.PostAnnotationsCmd) Response { @@ -53,14 +42,13 @@ func PostAnnotation(c *middleware.Context, cmd dtos.PostAnnotationsCmd) Response item := annotations.Item{ OrgId: c.OrgId, + UserId: c.UserId, DashboardId: cmd.DashboardId, PanelId: cmd.PanelId, Epoch: cmd.Time / 1000, - Title: cmd.Title, Text: cmd.Text, - CategoryId: cmd.CategoryId, - NewState: cmd.FillColor, - Type: annotations.EventType, + Data: cmd.Data, + Tags: cmd.Tags, } if err := repo.Save(&item); err != nil { @@ -71,12 +59,16 @@ func PostAnnotation(c *middleware.Context, cmd dtos.PostAnnotationsCmd) Response if cmd.IsRegion { item.RegionId = item.Id + if item.Data == nil { + item.Data = simplejson.New() + } + if err := repo.Update(&item); err != nil { return ApiError(500, "Failed set regionId on annotation", err) } item.Id = 0 - item.Epoch = cmd.TimeEnd + item.Epoch = cmd.TimeEnd / 1000 if err := repo.Save(&item); err != nil { return ApiError(500, "Failed save annotation for region end time", err) @@ -86,6 +78,41 @@ func PostAnnotation(c *middleware.Context, cmd dtos.PostAnnotationsCmd) Response return ApiSuccess("Annotation added") } +func UpdateAnnotation(c *middleware.Context, cmd dtos.UpdateAnnotationsCmd) Response { + annotationId := c.ParamsInt64(":annotationId") + + repo := annotations.GetRepository() + + item := annotations.Item{ + OrgId: c.OrgId, + UserId: c.UserId, + Id: annotationId, + Epoch: cmd.Time / 1000, + Text: cmd.Text, + Tags: cmd.Tags, + } + + if err := repo.Update(&item); err != nil { + return ApiError(500, "Failed to update annotation", err) + } + + if cmd.IsRegion { + itemRight := item + itemRight.RegionId = item.Id + itemRight.Epoch = cmd.TimeEnd / 1000 + + // We don't know id of region right event, so set it to 0 and find then using query like + // ... WHERE region_id = AND id != ... + itemRight.Id = 0 + + if err := repo.Update(&itemRight); err != nil { + return ApiError(500, "Failed to update annotation for region end time", err) + } + } + + return ApiSuccess("Annotation updated") +} + func DeleteAnnotations(c *middleware.Context, cmd dtos.DeleteAnnotationsCmd) Response { repo := annotations.GetRepository() @@ -101,3 +128,33 @@ func DeleteAnnotations(c *middleware.Context, cmd dtos.DeleteAnnotationsCmd) Res return ApiSuccess("Annotations deleted") } + +func DeleteAnnotationById(c *middleware.Context) Response { + repo := annotations.GetRepository() + annotationId := c.ParamsInt64(":annotationId") + + err := repo.Delete(&annotations.DeleteParams{ + Id: annotationId, + }) + + if err != nil { + return ApiError(500, "Failed to delete annotation", err) + } + + return ApiSuccess("Annotation deleted") +} + +func DeleteAnnotationRegion(c *middleware.Context) Response { + repo := annotations.GetRepository() + regionId := c.ParamsInt64(":regionId") + + err := repo.Delete(&annotations.DeleteParams{ + RegionId: regionId, + }) + + if err != nil { + return ApiError(500, "Failed to delete annotation region", err) + } + + return ApiSuccess("Annotation region deleted") +} diff --git a/pkg/api/api.go b/pkg/api/api.go index a979363d528..802d6e2d028 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -289,6 +289,9 @@ func (hs *HttpServer) registerRoutes() { apiRoute.Group("/annotations", func(annotationsRoute RouteRegister) { annotationsRoute.Post("/", bind(dtos.PostAnnotationsCmd{}), wrap(PostAnnotation)) + annotationsRoute.Delete("/:annotationId", wrap(DeleteAnnotationById)) + annotationsRoute.Put("/:annotationId", bind(dtos.UpdateAnnotationsCmd{}), wrap(UpdateAnnotation)) + annotationsRoute.Delete("/region/:regionId", wrap(DeleteAnnotationRegion)) }, reqEditorRole) // error test diff --git a/pkg/api/avatar/avatar.go b/pkg/api/avatar/avatar.go index 7abb5da1cec..80280fd3cc9 100644 --- a/pkg/api/avatar/avatar.go +++ b/pkg/api/avatar/avatar.go @@ -65,7 +65,7 @@ func New(hash string) *Avatar { return &Avatar{ hash: hash, reqParams: url.Values{ - "d": {"404"}, + "d": {"retro"}, "size": {"200"}, "r": {"pg"}}.Encode(), } @@ -146,7 +146,7 @@ func CacheServer() http.Handler { } func newNotFound() *Avatar { - avatar := &Avatar{} + avatar := &Avatar{notFound: true} // load transparent png into buffer path := filepath.Join(setting.StaticRootPath, "img", "transparent.png") diff --git a/pkg/api/dtos/annotations.go b/pkg/api/dtos/annotations.go index 958fdff89ca..ee5f6915b66 100644 --- a/pkg/api/dtos/annotations.go +++ b/pkg/api/dtos/annotations.go @@ -2,37 +2,30 @@ package dtos import "github.com/grafana/grafana/pkg/components/simplejson" -type Annotation struct { - AlertId int64 `json:"alertId"` - DashboardId int64 `json:"dashboardId"` - PanelId int64 `json:"panelId"` - NewState string `json:"newState"` - PrevState string `json:"prevState"` - Time int64 `json:"time"` - Title string `json:"title"` - Text string `json:"text"` - Metric string `json:"metric"` - RegionId int64 `json:"regionId"` - Type string `json:"type"` - - Data *simplejson.Json `json:"data"` +type PostAnnotationsCmd struct { + DashboardId int64 `json:"dashboardId"` + PanelId int64 `json:"panelId"` + Time int64 `json:"time"` + Text string `json:"text"` + Tags []string `json:"tags"` + Data *simplejson.Json `json:"data"` + IsRegion bool `json:"isRegion"` + TimeEnd int64 `json:"timeEnd"` } -type PostAnnotationsCmd struct { - DashboardId int64 `json:"dashboardId"` - PanelId int64 `json:"panelId"` - CategoryId int64 `json:"categoryId"` - Time int64 `json:"time"` - Title string `json:"title"` - Text string `json:"text"` - - FillColor string `json:"fillColor"` - IsRegion bool `json:"isRegion"` - TimeEnd int64 `json:"timeEnd"` +type UpdateAnnotationsCmd struct { + Id int64 `json:"id"` + Time int64 `json:"time"` + Text string `json:"text"` + Tags []string `json:"tags"` + IsRegion bool `json:"isRegion"` + TimeEnd int64 `json:"timeEnd"` } type DeleteAnnotationsCmd struct { - AlertId int64 `json:"alertId"` - DashboardId int64 `json:"dashboardId"` - PanelId int64 `json:"panelId"` + AlertId int64 `json:"alertId"` + DashboardId int64 `json:"dashboardId"` + PanelId int64 `json:"panelId"` + AnnotationId int64 `json:"annotationId"` + RegionId int64 `json:"regionId"` } diff --git a/pkg/models/tags.go b/pkg/models/tags.go new file mode 100644 index 00000000000..1b90b7d55ba --- /dev/null +++ b/pkg/models/tags.go @@ -0,0 +1,60 @@ +package models + +import ( + "strings" +) + +type Tag struct { + Id int64 + Key string + Value string +} + +func ParseTagPairs(tagPairs []string) (tags []*Tag) { + if tagPairs == nil { + return []*Tag{} + } + + for _, tagPair := range tagPairs { + var tag Tag + + if strings.Contains(tagPair, ":") { + keyValue := strings.Split(tagPair, ":") + tag.Key = strings.Trim(keyValue[0], " ") + tag.Value = strings.Trim(keyValue[1], " ") + } else { + tag.Key = strings.Trim(tagPair, " ") + } + + if tag.Key == "" || ContainsTag(tags, &tag) { + continue + } + + tags = append(tags, &tag) + } + + return tags +} + +func ContainsTag(existingTags []*Tag, tag *Tag) bool { + for _, t := range existingTags { + if t.Key == tag.Key && t.Value == tag.Value { + return true + } + } + return false +} + +func JoinTagPairs(tags []*Tag) []string { + tagPairs := []string{} + + for _, tag := range tags { + if tag.Value != "" { + tagPairs = append(tagPairs, tag.Key+":"+tag.Value) + } else { + tagPairs = append(tagPairs, tag.Key) + } + } + + return tagPairs +} diff --git a/pkg/models/tags_test.go b/pkg/models/tags_test.go new file mode 100644 index 00000000000..7d95187d668 --- /dev/null +++ b/pkg/models/tags_test.go @@ -0,0 +1,95 @@ +package models + +import ( + "testing" + + . "github.com/smartystreets/goconvey/convey" +) + +func TestParsingTags(t *testing.T) { + Convey("Testing parsing a tag pairs into tags", t, func() { + Convey("Can parse one empty tag", func() { + tags := ParseTagPairs([]string{""}) + So(len(tags), ShouldEqual, 0) + }) + + Convey("Can parse valid tags", func() { + tags := ParseTagPairs([]string{"outage", "type:outage", "error"}) + So(len(tags), ShouldEqual, 3) + So(tags[0].Key, ShouldEqual, "outage") + So(tags[0].Value, ShouldEqual, "") + So(tags[1].Key, ShouldEqual, "type") + So(tags[1].Value, ShouldEqual, "outage") + So(tags[2].Key, ShouldEqual, "error") + So(tags[2].Value, ShouldEqual, "") + }) + + Convey("Can parse tags with spaces", func() { + tags := ParseTagPairs([]string{" outage ", " type : outage ", "error "}) + So(len(tags), ShouldEqual, 3) + So(tags[0].Key, ShouldEqual, "outage") + So(tags[0].Value, ShouldEqual, "") + So(tags[1].Key, ShouldEqual, "type") + So(tags[1].Value, ShouldEqual, "outage") + So(tags[2].Key, ShouldEqual, "error") + So(tags[2].Value, ShouldEqual, "") + }) + + Convey("Can parse empty tags", func() { + tags := ParseTagPairs([]string{" outage ", "", "", ":", "type : outage ", "error ", "", ""}) + So(len(tags), ShouldEqual, 3) + So(tags[0].Key, ShouldEqual, "outage") + So(tags[0].Value, ShouldEqual, "") + So(tags[1].Key, ShouldEqual, "type") + So(tags[1].Value, ShouldEqual, "outage") + So(tags[2].Key, ShouldEqual, "error") + So(tags[2].Value, ShouldEqual, "") + }) + + Convey("Can parse tags with extra colons", func() { + tags := ParseTagPairs([]string{" outage", "type : outage:outage2 :outage3 ", "error :"}) + So(len(tags), ShouldEqual, 3) + So(tags[0].Key, ShouldEqual, "outage") + So(tags[0].Value, ShouldEqual, "") + So(tags[1].Key, ShouldEqual, "type") + So(tags[1].Value, ShouldEqual, "outage") + So(tags[2].Key, ShouldEqual, "error") + So(tags[2].Value, ShouldEqual, "") + }) + + Convey("Can parse tags that contains key and values with spaces", func() { + tags := ParseTagPairs([]string{" outage 1", "type 1: outage 1 ", "has error "}) + So(len(tags), ShouldEqual, 3) + So(tags[0].Key, ShouldEqual, "outage 1") + So(tags[0].Value, ShouldEqual, "") + So(tags[1].Key, ShouldEqual, "type 1") + So(tags[1].Value, ShouldEqual, "outage 1") + So(tags[2].Key, ShouldEqual, "has error") + So(tags[2].Value, ShouldEqual, "") + }) + + Convey("Can filter out duplicate tags", func() { + tags := ParseTagPairs([]string{"test", "test", "key:val1", "key:val2"}) + So(len(tags), ShouldEqual, 3) + So(tags[0].Key, ShouldEqual, "test") + So(tags[0].Value, ShouldEqual, "") + So(tags[1].Key, ShouldEqual, "key") + So(tags[1].Value, ShouldEqual, "val1") + So(tags[2].Key, ShouldEqual, "key") + So(tags[2].Value, ShouldEqual, "val2") + }) + + Convey("Can join tag pairs", func() { + tagPairs := []*Tag{ + {Key: "key1", Value: "val1"}, + {Key: "key2", Value: ""}, + {Key: "key3"}, + } + tags := JoinTagPairs(tagPairs) + So(len(tags), ShouldEqual, 3) + So(tags[0], ShouldEqual, "key1:val1") + So(tags[1], ShouldEqual, "key2") + So(tags[2], ShouldEqual, "key3") + }) + }) +} diff --git a/pkg/services/alerting/result_handler.go b/pkg/services/alerting/result_handler.go index d34dbf5a632..448b4ace5bb 100644 --- a/pkg/services/alerting/result_handler.go +++ b/pkg/services/alerting/result_handler.go @@ -73,10 +73,8 @@ func (handler *DefaultResultHandler) Handle(evalContext *EvalContext) error { OrgId: evalContext.Rule.OrgId, DashboardId: evalContext.Rule.DashboardId, PanelId: evalContext.Rule.PanelId, - Type: annotations.AlertType, AlertId: evalContext.Rule.Id, - Title: evalContext.Rule.Name, - Text: evalContext.GetStateModel().Text, + Text: "", NewState: string(evalContext.Rule.State), PrevState: string(evalContext.PrevAlertState), Epoch: time.Now().Unix(), diff --git a/pkg/services/annotations/annotations.go b/pkg/services/annotations/annotations.go index be9d3f2d4d0..2fdc824f172 100644 --- a/pkg/services/annotations/annotations.go +++ b/pkg/services/annotations/annotations.go @@ -5,7 +5,7 @@ import "github.com/grafana/grafana/pkg/components/simplejson" type Repository interface { Save(item *Item) error Update(item *Item) error - Find(query *ItemQuery) ([]*Item, error) + Find(query *ItemQuery) ([]*ItemDTO, error) Delete(params *DeleteParams) error } @@ -13,11 +13,10 @@ type ItemQuery struct { OrgId int64 `json:"orgId"` From int64 `json:"from"` To int64 `json:"to"` - Type ItemType `json:"type"` AlertId int64 `json:"alertId"` DashboardId int64 `json:"dashboardId"` PanelId int64 `json:"panelId"` - NewState []string `json:"newState"` + Tags []string `json:"tags"` Limit int64 `json:"limit"` } @@ -28,12 +27,15 @@ type PostParams struct { Epoch int64 `json:"epoch"` Title string `json:"title"` Text string `json:"text"` + Icon string `json:"icon"` } type DeleteParams struct { + Id int64 `json:"id"` AlertId int64 `json:"alertId"` DashboardId int64 `json:"dashboardId"` PanelId int64 `json:"panelId"` + RegionId int64 `json:"regionId"` } var repositoryInstance Repository @@ -46,29 +48,41 @@ func SetRepository(rep Repository) { repositoryInstance = rep } -type ItemType string - -const ( - AlertType ItemType = "alert" - EventType ItemType = "event" -) - type Item struct { - Id int64 `json:"id"` - OrgId int64 `json:"orgId"` - DashboardId int64 `json:"dashboardId"` - PanelId int64 `json:"panelId"` - CategoryId int64 `json:"categoryId"` - RegionId int64 `json:"regionId"` - Type ItemType `json:"type"` - Title string `json:"title"` - Text string `json:"text"` - Metric string `json:"metric"` - AlertId int64 `json:"alertId"` - UserId int64 `json:"userId"` - PrevState string `json:"prevState"` - NewState string `json:"newState"` - Epoch int64 `json:"epoch"` + Id int64 `json:"id"` + OrgId int64 `json:"orgId"` + UserId int64 `json:"userId"` + DashboardId int64 `json:"dashboardId"` + PanelId int64 `json:"panelId"` + RegionId int64 `json:"regionId"` + Text string `json:"text"` + AlertId int64 `json:"alertId"` + PrevState string `json:"prevState"` + NewState string `json:"newState"` + Epoch int64 `json:"epoch"` + Tags []string `json:"tags"` + Data *simplejson.Json `json:"data"` - Data *simplejson.Json `json:"data"` + // needed until we remove it from db + Type string + Title string +} + +type ItemDTO struct { + Id int64 `json:"id"` + AlertId int64 `json:"alertId"` + AlertName string `json:"alertName"` + DashboardId int64 `json:"dashboardId"` + PanelId int64 `json:"panelId"` + UserId int64 `json:"userId"` + NewState string `json:"newState"` + PrevState string `json:"prevState"` + Time int64 `json:"time"` + Text string `json:"text"` + RegionId int64 `json:"regionId"` + Tags []string `json:"tags"` + Login string `json:"login"` + Email string `json:"email"` + AvatarUrl string `json:"avatarUrl"` + Data *simplejson.Json `json:"data"` } diff --git a/pkg/services/sqlstore/annotation.go b/pkg/services/sqlstore/annotation.go index ffad5bf2cad..a2c5d80ac3a 100644 --- a/pkg/services/sqlstore/annotation.go +++ b/pkg/services/sqlstore/annotation.go @@ -2,9 +2,11 @@ package sqlstore import ( "bytes" + "errors" "fmt" "strings" + "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/annotations" ) @@ -13,19 +15,94 @@ type SqlAnnotationRepo struct { func (r *SqlAnnotationRepo) Save(item *annotations.Item) error { return inTransaction(func(sess *DBSession) error { - + tags := models.ParseTagPairs(item.Tags) + item.Tags = models.JoinTagPairs(tags) if _, err := sess.Table("annotation").Insert(item); err != nil { return err } + if item.Tags != nil { + if tags, err := r.ensureTagsExist(sess, tags); err != nil { + return err + } else { + for _, tag := range tags { + if _, err := sess.Exec("INSERT INTO annotation_tag (annotation_id, tag_id) VALUES(?,?)", item.Id, tag.Id); err != nil { + return err + } + } + } + } + return nil }) } +// Will insert if needed any new key/value pars and return ids +func (r *SqlAnnotationRepo) ensureTagsExist(sess *DBSession, tags []*models.Tag) ([]*models.Tag, error) { + for _, tag := range tags { + var existingTag models.Tag + + // check if it exists + if exists, err := sess.Table("tag").Where("key=? AND value=?", tag.Key, tag.Value).Get(&existingTag); err != nil { + return nil, err + } else if exists { + tag.Id = existingTag.Id + } else { + if _, err := sess.Table("tag").Insert(tag); err != nil { + return nil, err + } + } + } + + return tags, nil +} + func (r *SqlAnnotationRepo) Update(item *annotations.Item) error { return inTransaction(func(sess *DBSession) error { + var ( + isExist bool + err error + ) + existing := new(annotations.Item) - if _, err := sess.Table("annotation").Id(item.Id).Update(item); err != nil { + if item.Id == 0 && item.RegionId != 0 { + // Update region end time + isExist, err = sess.Table("annotation").Where("region_id=? AND id!=? AND org_id=?", item.RegionId, item.RegionId, item.OrgId).Get(existing) + } else { + isExist, err = sess.Table("annotation").Where("id=? AND org_id=?", item.Id, item.OrgId).Get(existing) + } + + if err != nil { + return err + } + if !isExist { + return errors.New("Annotation not found") + } + + existing.Epoch = item.Epoch + existing.Text = item.Text + if item.RegionId != 0 { + existing.RegionId = item.RegionId + } + + if item.Tags != nil { + if tags, err := r.ensureTagsExist(sess, models.ParseTagPairs(item.Tags)); err != nil { + return err + } else { + if _, err := sess.Exec("DELETE FROM annotation_tag WHERE annotation_id = ?", existing.Id); err != nil { + return err + } + for _, tag := range tags { + if _, err := sess.Exec("INSERT INTO annotation_tag (annotation_id, tag_id) VALUES(?,?)", existing.Id, tag.Id); err != nil { + return err + } + } + } + } + + existing.Tags = item.Tags + + if _, err := sess.Table("annotation").Id(existing.Id).Cols("epoch", "text", "region_id", "tags").Update(existing); err != nil { return err } @@ -33,51 +110,79 @@ func (r *SqlAnnotationRepo) Update(item *annotations.Item) error { }) } -func (r *SqlAnnotationRepo) Find(query *annotations.ItemQuery) ([]*annotations.Item, error) { +func (r *SqlAnnotationRepo) Find(query *annotations.ItemQuery) ([]*annotations.ItemDTO, error) { var sql bytes.Buffer params := make([]interface{}, 0) - sql.WriteString(`SELECT * - from annotation - `) + sql.WriteString(` + SELECT + annotation.id, + annotation.epoch as time, + annotation.dashboard_id, + annotation.panel_id, + annotation.new_state, + annotation.prev_state, + annotation.alert_id, + annotation.region_id, + annotation.text, + annotation.tags, + annotation.data, + usr.email, + usr.login, + alert.name as alert_name + FROM annotation + LEFT OUTER JOIN ` + dialect.Quote("user") + ` as usr on usr.id = annotation.user_id + LEFT OUTER JOIN alert on alert.id = annotation.alert_id + `) - sql.WriteString(`WHERE org_id = ?`) + sql.WriteString(`WHERE annotation.org_id = ?`) params = append(params, query.OrgId) if query.AlertId != 0 { - sql.WriteString(` AND alert_id = ?`) - params = append(params, query.AlertId) - } - - if query.AlertId != 0 { - sql.WriteString(` AND alert_id = ?`) + sql.WriteString(` AND annotation.alert_id = ?`) params = append(params, query.AlertId) } if query.DashboardId != 0 { - sql.WriteString(` AND dashboard_id = ?`) + sql.WriteString(` AND annotation.dashboard_id = ?`) params = append(params, query.DashboardId) } if query.PanelId != 0 { - sql.WriteString(` AND panel_id = ?`) + sql.WriteString(` AND annotation.panel_id = ?`) params = append(params, query.PanelId) } if query.From > 0 && query.To > 0 { - sql.WriteString(` AND epoch BETWEEN ? AND ?`) + sql.WriteString(` AND annotation.epoch BETWEEN ? AND ?`) params = append(params, query.From, query.To) } - if query.Type != "" { - sql.WriteString(` AND type = ?`) - params = append(params, string(query.Type)) - } + if len(query.Tags) > 0 { + keyValueFilters := []string{} - if len(query.NewState) > 0 { - sql.WriteString(` AND new_state IN (?` + strings.Repeat(",?", len(query.NewState)-1) + ")") - for _, v := range query.NewState { - params = append(params, v) + tags := models.ParseTagPairs(query.Tags) + for _, tag := range tags { + if tag.Value == "" { + keyValueFilters = append(keyValueFilters, "(tag.key = ?)") + params = append(params, tag.Key) + } else { + keyValueFilters = append(keyValueFilters, "(tag.key = ? AND tag.value = ?)") + params = append(params, tag.Key, tag.Value) + } + } + + if len(tags) > 0 { + tagsSubQuery := fmt.Sprintf(` + SELECT SUM(1) FROM annotation_tag at + INNER JOIN tag on tag.id = at.tag_id + WHERE at.annotation_id = annotation.id + AND ( + %s + ) + `, strings.Join(keyValueFilters, " OR ")) + + sql.WriteString(fmt.Sprintf(" AND (%s) = %d ", tagsSubQuery, len(tags))) } } @@ -87,7 +192,7 @@ func (r *SqlAnnotationRepo) Find(query *annotations.ItemQuery) ([]*annotations.I sql.WriteString(fmt.Sprintf(" ORDER BY epoch DESC LIMIT %v", query.Limit)) - items := make([]*annotations.Item, 0) + items := make([]*annotations.ItemDTO, 0) if err := x.Sql(sql.String(), params...).Find(&items); err != nil { return nil, err } @@ -97,11 +202,31 @@ func (r *SqlAnnotationRepo) Find(query *annotations.ItemQuery) ([]*annotations.I func (r *SqlAnnotationRepo) Delete(params *annotations.DeleteParams) error { return inTransaction(func(sess *DBSession) error { + var ( + sql string + annoTagSql string + queryParams []interface{} + ) - sql := "DELETE FROM annotation WHERE dashboard_id = ? AND panel_id = ?" + if params.RegionId != 0 { + annoTagSql = "DELETE FROM annotation_tag WHERE annotation_id IN (SELECT id FROM annotation WHERE region_id = ?)" + sql = "DELETE FROM annotation WHERE region_id = ?" + queryParams = []interface{}{params.RegionId} + } else if params.Id != 0 { + annoTagSql = "DELETE FROM annotation_tag WHERE annotation_id IN (SELECT id FROM annotation WHERE id = ?)" + sql = "DELETE FROM annotation WHERE id = ?" + queryParams = []interface{}{params.Id} + } else { + annoTagSql = "DELETE FROM annotation_tag WHERE annotation_id IN (SELECT id FROM annotation WHERE dashboard_id = ? AND panel_id = ?)" + sql = "DELETE FROM annotation WHERE dashboard_id = ? AND panel_id = ?" + queryParams = []interface{}{params.DashboardId, params.PanelId} + } - _, err := sess.Exec(sql, params.DashboardId, params.PanelId) - if err != nil { + if _, err := sess.Exec(annoTagSql, queryParams...); err != nil { + return err + } + + if _, err := sess.Exec(sql, queryParams...); err != nil { return err } diff --git a/pkg/services/sqlstore/annotation_test.go b/pkg/services/sqlstore/annotation_test.go new file mode 100644 index 00000000000..3f7415a952b --- /dev/null +++ b/pkg/services/sqlstore/annotation_test.go @@ -0,0 +1,208 @@ +package sqlstore + +import ( + "testing" + + . "github.com/smartystreets/goconvey/convey" + + "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/services/annotations" +) + +func TestSavingTags(t *testing.T) { + Convey("Testing annotation saving/loading", t, func() { + InitTestDB(t) + + repo := SqlAnnotationRepo{} + + Convey("Can save tags", func() { + tagPairs := []*models.Tag{ + {Key: "outage"}, + {Key: "type", Value: "outage"}, + {Key: "server", Value: "server-1"}, + {Key: "error"}, + } + tags, err := repo.ensureTagsExist(newSession(), tagPairs) + + So(err, ShouldBeNil) + So(len(tags), ShouldEqual, 4) + }) + }) +} + +func TestAnnotations(t *testing.T) { + Convey("Testing annotation saving/loading", t, func() { + InitTestDB(t) + + repo := SqlAnnotationRepo{} + + Convey("Can save annotation", func() { + err := repo.Save(&annotations.Item{ + OrgId: 1, + UserId: 1, + DashboardId: 1, + Text: "hello", + Epoch: 10, + Tags: []string{"outage", "error", "type:outage", "server:server-1"}, + }) + + So(err, ShouldBeNil) + + Convey("Can query for annotation", func() { + items, err := repo.Find(&annotations.ItemQuery{ + OrgId: 1, + DashboardId: 1, + From: 0, + To: 15, + }) + + So(err, ShouldBeNil) + So(items, ShouldHaveLength, 1) + + Convey("Can read tags", func() { + So(items[0].Tags, ShouldResemble, []string{"outage", "error", "type:outage", "server:server-1"}) + }) + }) + + Convey("Should not find any when item is outside time range", func() { + items, err := repo.Find(&annotations.ItemQuery{ + OrgId: 1, + DashboardId: 1, + From: 12, + To: 15, + }) + + So(err, ShouldBeNil) + So(items, ShouldHaveLength, 0) + }) + + Convey("Should not find one when tag filter does not match", func() { + items, err := repo.Find(&annotations.ItemQuery{ + OrgId: 1, + DashboardId: 1, + From: 1, + To: 15, + Tags: []string{"asd"}, + }) + + So(err, ShouldBeNil) + So(items, ShouldHaveLength, 0) + }) + + Convey("Should find one when all tag filters does match", func() { + items, err := repo.Find(&annotations.ItemQuery{ + OrgId: 1, + DashboardId: 1, + From: 1, + To: 15, + Tags: []string{"outage", "error"}, + }) + + So(err, ShouldBeNil) + So(items, ShouldHaveLength, 1) + }) + + Convey("Should find one when all key value tag filters does match", func() { + items, err := repo.Find(&annotations.ItemQuery{ + OrgId: 1, + DashboardId: 1, + From: 1, + To: 15, + Tags: []string{"type:outage", "server:server-1"}, + }) + + So(err, ShouldBeNil) + So(items, ShouldHaveLength, 1) + }) + + Convey("Can update annotation and remove all tags", func() { + query := &annotations.ItemQuery{ + OrgId: 1, + DashboardId: 1, + From: 0, + To: 15, + } + items, err := repo.Find(query) + + So(err, ShouldBeNil) + + annotationId := items[0].Id + + err = repo.Update(&annotations.Item{ + Id: annotationId, + OrgId: 1, + Text: "something new", + Tags: []string{}, + }) + + So(err, ShouldBeNil) + + items, err = repo.Find(query) + + So(err, ShouldBeNil) + + Convey("Can read tags", func() { + So(items[0].Id, ShouldEqual, annotationId) + So(len(items[0].Tags), ShouldEqual, 0) + So(items[0].Text, ShouldEqual, "something new") + }) + }) + + Convey("Can update annotation with new tags", func() { + query := &annotations.ItemQuery{ + OrgId: 1, + DashboardId: 1, + From: 0, + To: 15, + } + items, err := repo.Find(query) + + So(err, ShouldBeNil) + + annotationId := items[0].Id + + err = repo.Update(&annotations.Item{ + Id: annotationId, + OrgId: 1, + Text: "something new", + Tags: []string{"newtag1", "newtag2"}, + }) + + So(err, ShouldBeNil) + + items, err = repo.Find(query) + + So(err, ShouldBeNil) + + Convey("Can read tags", func() { + So(items[0].Id, ShouldEqual, annotationId) + So(items[0].Tags, ShouldResemble, []string{"newtag1", "newtag2"}) + So(items[0].Text, ShouldEqual, "something new") + }) + }) + + Convey("Can delete annotation", func() { + query := &annotations.ItemQuery{ + OrgId: 1, + DashboardId: 1, + From: 0, + To: 15, + } + items, err := repo.Find(query) + So(err, ShouldBeNil) + + annotationId := items[0].Id + + err = repo.Delete(&annotations.DeleteParams{Id: annotationId}) + + items, err = repo.Find(query) + So(err, ShouldBeNil) + + Convey("Should be deleted", func() { + So(len(items), ShouldEqual, 0) + }) + }) + + }) + }) +} diff --git a/pkg/services/sqlstore/dashboard.go b/pkg/services/sqlstore/dashboard.go index 27812eef32e..d91b4a08aa6 100644 --- a/pkg/services/sqlstore/dashboard.go +++ b/pkg/services/sqlstore/dashboard.go @@ -261,6 +261,7 @@ func DeleteDashboard(cmd *m.DeleteDashboardCommand) error { "DELETE FROM dashboard WHERE id = ?", "DELETE FROM playlist_item WHERE type = 'dashboard_by_id' AND value = ?", "DELETE FROM dashboard_version WHERE dashboard_id = ?", + "DELETE FROM annotation WHERE dashboard_id = ?", } for _, sql := range deletes { diff --git a/pkg/services/sqlstore/migrations/annotation_mig.go b/pkg/services/sqlstore/migrations/annotation_mig.go index a9343266863..8d2bf94bc42 100644 --- a/pkg/services/sqlstore/migrations/annotation_mig.go +++ b/pkg/services/sqlstore/migrations/annotation_mig.go @@ -57,4 +57,37 @@ func addAnnotationMig(mg *Migrator) { mg.AddMigration("Add column region_id to annotation table", NewAddColumnMigration(table, &Column{ Name: "region_id", Type: DB_BigInt, Nullable: true, Default: "0", })) + + categoryIdIndex := &Index{Cols: []string{"org_id", "category_id"}, Type: IndexType} + mg.AddMigration("Drop category_id index", NewDropIndexMigration(table, categoryIdIndex)) + + mg.AddMigration("Add column tags to annotation table", NewAddColumnMigration(table, &Column{ + Name: "tags", Type: DB_NVarchar, Nullable: true, Length: 500, + })) + + /// + /// Annotation tag + /// + annotationTagTable := Table{ + Name: "annotation_tag", + Columns: []*Column{ + {Name: "annotation_id", Type: DB_BigInt, Nullable: false}, + {Name: "tag_id", Type: DB_BigInt, Nullable: false}, + }, + Indices: []*Index{ + {Cols: []string{"annotation_id", "tag_id"}, Type: UniqueIndex}, + }, + } + + mg.AddMigration("Create annotation_tag table v2", NewAddTableMigration(annotationTagTable)) + mg.AddMigration("Add unique index annotation_tag.annotation_id_tag_id", NewAddIndexMigration(annotationTagTable, annotationTagTable.Indices[0])) + + // + // clear alert text + // + updateTextFieldSql := "UPDATE annotation SET TEXT = '' WHERE alert_id > 0" + mg.AddMigration("Update alert annotations and set TEXT to empty", new(RawSqlMigration). + Sqlite(updateTextFieldSql). + Postgres(updateTextFieldSql). + Mysql(updateTextFieldSql)) } diff --git a/pkg/services/sqlstore/migrations/migrations.go b/pkg/services/sqlstore/migrations/migrations.go index 38072fe88e4..4984ff18592 100644 --- a/pkg/services/sqlstore/migrations/migrations.go +++ b/pkg/services/sqlstore/migrations/migrations.go @@ -26,6 +26,7 @@ func AddMigrations(mg *Migrator) { addAnnotationMig(mg) addTestDataMigrations(mg) addDashboardVersionMigration(mg) + addTagMigration(mg) } func addMigrationLogMigrations(mg *Migrator) { diff --git a/pkg/services/sqlstore/migrations/tag_mig.go b/pkg/services/sqlstore/migrations/tag_mig.go new file mode 100644 index 00000000000..0303ddd6409 --- /dev/null +++ b/pkg/services/sqlstore/migrations/tag_mig.go @@ -0,0 +1,24 @@ +package migrations + +import . "github.com/grafana/grafana/pkg/services/sqlstore/migrator" + +func addTagMigration(mg *Migrator) { + + tagTable := Table{ + Name: "tag", + Columns: []*Column{ + {Name: "id", Type: DB_BigInt, IsPrimaryKey: true, IsAutoIncrement: true}, + {Name: "key", Type: DB_NVarchar, Length: 100, Nullable: false}, + {Name: "value", Type: DB_NVarchar, Length: 100, Nullable: false}, + }, + Indices: []*Index{ + {Cols: []string{"key", "value"}, Type: UniqueIndex}, + }, + } + + // create table + mg.AddMigration("create tag table", NewAddTableMigration(tagTable)) + + // create indices + mg.AddMigration("add index tag.key_value", NewAddIndexMigration(tagTable, tagTable.Indices[0])) +} diff --git a/public/app/core/components/dashboard_selector.ts b/public/app/core/components/dashboard_selector.ts index f68e70a17c0..7ec9f681520 100644 --- a/public/app/core/components/dashboard_selector.ts +++ b/public/app/core/components/dashboard_selector.ts @@ -4,9 +4,6 @@ import coreModule from 'app/core/core_module'; var template = ` - - Not finding dashboard you want? Star it first, then it should appear in this select box. - `; export class DashboardSelectorCtrl { diff --git a/public/app/core/components/grafana_app.ts b/public/app/core/components/grafana_app.ts index 0eee3ae43fd..8852da4a436 100644 --- a/public/app/core/components/grafana_app.ts +++ b/public/app/core/components/grafana_app.ts @@ -7,6 +7,7 @@ import $ from 'jquery'; import coreModule from 'app/core/core_module'; import {profiler} from 'app/core/profiler'; import appEvents from 'app/core/app_events'; +import Drop from 'tether-drop'; export class GrafanaCtrl { @@ -117,6 +118,11 @@ export function grafanaAppDirective(playlistSrv, contextSrv) { if (data.params.kiosk) { appEvents.emit('toggle-kiosk-mode'); } + + // close all drops + for (let drop of Drop.drops) { + drop.destroy(); + } }); // handle kiosk mode diff --git a/public/app/core/components/info_popover.ts b/public/app/core/components/info_popover.ts index a6ea853b7bb..954e84a3baa 100644 --- a/public/app/core/components/info_popover.ts +++ b/public/app/core/components/info_popover.ts @@ -27,6 +27,8 @@ export function infoPopover() { transclude(function(clone, newScope) { var content = document.createElement("div"); + content.className = 'markdown-html'; + _.each(clone, (node) => { content.appendChild(node); }); diff --git a/public/app/core/directives/tags.js b/public/app/core/directives/tags.js index 90a355dea07..a322673a342 100644 --- a/public/app/core/directives/tags.js +++ b/public/app/core/directives/tags.js @@ -88,6 +88,7 @@ function (angular, $, coreModule) { typeahead: { source: angular.isFunction(scope.$parent[attrs.typeaheadSource]) ? scope.$parent[attrs.typeaheadSource] : null }, + widthClass: attrs.widthClass, itemValue: getItemProperty(scope, attrs.itemvalue), itemText : getItemProperty(scope, attrs.itemtext), tagClass : angular.isFunction(scope.$parent[attrs.tagclass]) ? diff --git a/public/app/core/nav_model_srv.ts b/public/app/core/nav_model_srv.ts index 4771f8c191c..dd61db5d346 100644 --- a/public/app/core/nav_model_srv.ts +++ b/public/app/core/nav_model_srv.ts @@ -163,7 +163,7 @@ export class NavModelSrv { menu.push({ title: 'Annotations', - icon: 'fa fa-fw fa-bolt', + icon: 'fa fa-fw fa-comment', clickHandler: () => dashNavCtrl.openEditView('annotations') }); diff --git a/public/app/features/alerting/alert_def.ts b/public/app/features/alerting/alert_def.ts index 51cbbd9691f..c86f0dee775 100644 --- a/public/app/features/alerting/alert_def.ts +++ b/public/app/features/alerting/alert_def.ts @@ -128,7 +128,6 @@ function joinEvalMatches(matches, separator: string) { } function getAlertAnnotationInfo(ah) { - // backward compatability, can be removed in grafana 5.x // old way stored evalMatches in data property directly, // new way stores it in evalMatches property on new data object diff --git a/public/app/features/annotations/annotation_tooltip.ts b/public/app/features/annotations/annotation_tooltip.ts index 39c2ff84acb..c8c95b38392 100644 --- a/public/app/features/annotations/annotation_tooltip.ts +++ b/public/app/features/annotations/annotation_tooltip.ts @@ -1,12 +1,10 @@ -/// - import _ from 'lodash'; import $ from 'jquery'; import coreModule from 'app/core/core_module'; import alertDef from '../alerting/alert_def'; /** @ngInject **/ -export function annotationTooltipDirective($sanitize, dashboardSrv, $compile) { +export function annotationTooltipDirective($sanitize, dashboardSrv, contextSrv, popoverSrv, $compile) { function sanitizeString(str) { try { @@ -21,6 +19,7 @@ export function annotationTooltipDirective($sanitize, dashboardSrv, $compile) { restrict: 'E', scope: { "event": "=", + "onEdit": "&" }, link: function(scope, element) { var event = scope.event; @@ -31,33 +30,46 @@ export function annotationTooltipDirective($sanitize, dashboardSrv, $compile) { var tooltip = '
      '; var titleStateClass = ''; - if (event.source.name === 'panel-alert') { + if (event.alertId) { var stateModel = alertDef.getStateDisplayModel(event.newState); titleStateClass = stateModel.stateClass; title = ` ${stateModel.text}`; text = alertDef.getAlertAnnotationInfo(event); + if (event.text) { + text = text + '
      ' + event.text; + } + } else if (title) { + text = title + '
      ' + text; + title = ''; } - tooltip += ` -
      - ${sanitizeString(title)} - ${dashboard.formatDate(event.min)} -
      + var header = `
      `; + if (event.login) { + header += `
      `; + } + header += ` + ${sanitizeString(title)} + ${dashboard.formatDate(event.min)} `; - tooltip += '
      '; + // Show edit icon only for users with at least Editor role + if (event.id && contextSrv.isEditor) { + header += ` + + + + `; + } + + header += `
      `; + tooltip += header; + tooltip += '
      '; if (text) { - tooltip += sanitizeString(text).replace(/\n/g, '
      ') + '
      '; + tooltip += '
      ' + sanitizeString(text).replace(/\n/g, '
      ') + '
      '; } var tags = event.tags; - if (_.isString(event.tags)) { - tags = event.tags.split(','); - if (tags.length === 1) { - tags = event.tags.split(' '); - } - } if (tags && tags.length) { scope.tags = tags; @@ -65,6 +77,7 @@ export function annotationTooltipDirective($sanitize, dashboardSrv, $compile) { } tooltip += "
      "; + tooltip += '
      '; var $tooltip = $(tooltip); $tooltip.appendTo(element); diff --git a/public/app/features/annotations/annotations_srv.ts b/public/app/features/annotations/annotations_srv.ts index e6a4c8660ae..2863ecdd843 100644 --- a/public/app/features/annotations/annotations_srv.ts +++ b/public/app/features/annotations/annotations_srv.ts @@ -1,5 +1,3 @@ -/// - import './editor_ctrl'; import angular from 'angular'; @@ -11,11 +9,7 @@ export class AnnotationsSrv { alertStatesPromise: any; /** @ngInject */ - constructor(private $rootScope, - private $q, - private datasourceSrv, - private backendSrv, - private timeSrv) { + constructor(private $rootScope, private $q, private datasourceSrv, private backendSrv, private timeSrv) { $rootScope.onAppEvent('refresh', this.clearCache.bind(this), $rootScope); $rootScope.onAppEvent('dashboard-initialized', this.clearCache.bind(this), $rootScope); } @@ -26,64 +20,40 @@ export class AnnotationsSrv { } getAnnotations(options) { - return this.$q.all([ - this.getGlobalAnnotations(options), - this.getPanelAnnotations(options), - this.getAlertStates(options) - ]).then(results => { + return this.$q + .all([this.getGlobalAnnotations(options), this.getAlertStates(options)]) + .then(results => { + // combine the annotations and flatten results + var annotations = _.flattenDeep(results[0]); - // combine the annotations and flatten results - var annotations = _.flattenDeep([results[0], results[1]]); - - // filter out annotations that do not belong to requesting panel - annotations = _.filter(annotations, item => { - // shownIn === 1 requires annotation matching panel id - if (item.source.showIn === 1) { - if (item.panelId && options.panel.id === item.panelId) { - return true; + // filter out annotations that do not belong to requesting panel + annotations = _.filter(annotations, item => { + // if event has panel id and query is of type dashboard then panel and requesting panel id must match + if (item.panelId && item.source.type === 'dashboard') { + return item.panelId === options.panel.id; } - return false; + return true; + }); + + annotations = dedupAnnotations(annotations); + annotations = makeRegions(annotations, options); + + // look for alert state for this panel + var alertState = _.find(results[1], {panelId: options.panel.id}); + + return { + annotations: annotations, + alertState: alertState, + }; + }) + .catch(err => { + if (!err.message && err.data && err.data.message) { + err.message = err.data.message; } - return true; + console.log('AnnotationSrv.query error', err); + this.$rootScope.appEvent('alert-error', ['Annotation Query Failed', err.message || err]); + return []; }); - - // look for alert state for this panel - var alertState = _.find(results[2], {panelId: options.panel.id}); - - return { - annotations: annotations, - alertState: alertState, - }; - - }).catch(err => { - if (!err.message && err.data && err.data.message) { - err.message = err.data.message; - } - this.$rootScope.appEvent('alert-error', ['Annotation Query Failed', (err.message || err)]); - - return []; - }); - } - - getPanelAnnotations(options) { - var panel = options.panel; - var dashboard = options.dashboard; - - if (dashboard.id && panel && panel.alert) { - return this.backendSrv.get('/api/annotations', { - from: options.range.from.valueOf(), - to: options.range.to.valueOf(), - limit: 100, - panelId: panel.id, - dashboardId: dashboard.id, - }).then(results => { - // this built in annotation source name `panel-alert` is used in annotation tooltip - // to know that this annotation is from panel alert - return this.translateQueryResult({iconColor: '#AA0000', name: 'panel-alert'}, results); - }); - } - - return this.$q.when([]); } getAlertStates(options) { @@ -104,43 +74,55 @@ export class AnnotationsSrv { return this.alertStatesPromise; } - this.alertStatesPromise = this.backendSrv.get('/api/alerts/states-for-dashboard', {dashboardId: options.dashboard.id}); + this.alertStatesPromise = this.backendSrv.get('/api/alerts/states-for-dashboard', { + dashboardId: options.dashboard.id, + }); return this.alertStatesPromise; } getGlobalAnnotations(options) { var dashboard = options.dashboard; - if (dashboard.annotations.list.length === 0) { - return this.$q.when([]); - } - if (this.globalAnnotationsPromise) { return this.globalAnnotationsPromise; } - var annotations = _.filter(dashboard.annotations.list, {enable: true}); var range = this.timeSrv.timeRange(); + var promises = []; + + for (let annotation of dashboard.annotations.list) { + if (!annotation.enable) { + continue; + } - this.globalAnnotationsPromise = this.$q.all(_.map(annotations, annotation => { if (annotation.snapshotData) { return this.translateQueryResult(annotation, annotation.snapshotData); } - return this.datasourceSrv.get(annotation.datasource).then(datasource => { - // issue query against data source - return datasource.annotationQuery({range: range, rangeRaw: range.raw, annotation: annotation}); - }) - .then(results => { - // store response in annotation object if this is a snapshot call - if (dashboard.snapshot) { - annotation.snapshotData = angular.copy(results); - } - // translate result - return this.translateQueryResult(annotation, results); - }); - })); + promises.push( + this.datasourceSrv + .get(annotation.datasource) + .then(datasource => { + // issue query against data source + return datasource.annotationQuery({ + range: range, + rangeRaw: range.raw, + annotation: annotation, + dashboard: dashboard, + }); + }) + .then(results => { + // store response in annotation object if this is a snapshot call + if (dashboard.snapshot) { + annotation.snapshotData = angular.copy(results); + } + // translate result + return this.translateQueryResult(annotation, results); + }), + ); + } + this.globalAnnotationsPromise = this.$q.all(promises); return this.globalAnnotationsPromise; } @@ -149,6 +131,21 @@ export class AnnotationsSrv { return this.backendSrv.post('/api/annotations', annotation); } + updateAnnotationEvent(annotation) { + this.globalAnnotationsPromise = null; + return this.backendSrv.put(`/api/annotations/${annotation.id}`, annotation); + } + + deleteAnnotationEvent(annotation) { + this.globalAnnotationsPromise = null; + let deleteUrl = `/api/annotations/${annotation.id}`; + if (annotation.isRegion) { + deleteUrl = `/api/annotations/region/${annotation.regionId}`; + } + + return this.backendSrv.delete(deleteUrl); + } + translateQueryResult(annotation, results) { // if annotation has snapshotData // make clone and remove it @@ -159,13 +156,88 @@ export class AnnotationsSrv { for (var item of results) { item.source = annotation; - item.min = item.time; - item.max = item.time; - item.scope = 1; - item.eventType = annotation.name; } return results; } } +/** + * This function converts annotation events into set + * of single events and regions (event consist of two) + * @param annotations + * @param options + */ +function makeRegions(annotations, options) { + let [regionEvents, singleEvents] = _.partition(annotations, 'regionId'); + let regions = getRegions(regionEvents, options.range); + annotations = _.concat(regions, singleEvents); + return annotations; +} + +function getRegions(events, range) { + let 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); + if (region_events && region_events.length > 1) { + region_obj.timeEnd = region_events[1].time; + region_obj.isRegion = true; + return region_obj; + } else { + if (region_events && region_events.length) { + // Don't change proper region object + if (!region_obj.time || !region_obj.timeEnd) { + // This is cut region + if (isStartOfRegion(region_obj)) { + region_obj.timeEnd = range.to.valueOf() - 1; + } else { + // Start time = null + region_obj.timeEnd = region_obj.time; + region_obj.time = range.from.valueOf() + 1; + } + region_obj.isRegion = true; + } + + return region_obj; + } + } + }), + ); + + return regions; +} + +function isStartOfRegion(event): boolean { + return event.id && event.id === event.regionId; +} + +function dedupAnnotations(annotations) { + let dedup = []; + + // Split events by annotationId property existance + let events = _.partition(annotations, 'id'); + + let eventsById = _.groupBy(events[0], 'id'); + dedup = _.map(eventsById, eventGroup => { + if (eventGroup.length > 1 && !_.every(eventGroup, isPanelAlert)) { + // Get first non-panel alert + return _.find(eventGroup, event => { + return event.eventType !== 'panel-alert'; + }); + } else { + return _.head(eventGroup); + } + }); + + dedup = _.concat(dedup, events[1]); + return dedup; +} + +function isPanelAlert(event) { + return event.eventType === 'panel-alert'; +} + coreModule.service('annotationsSrv', AnnotationsSrv); diff --git a/public/app/features/annotations/editor_ctrl.ts b/public/app/features/annotations/editor_ctrl.ts index 74c4768b5ad..fafb51aea08 100644 --- a/public/app/features/annotations/editor_ctrl.ts +++ b/public/app/features/annotations/editor_ctrl.ts @@ -1,5 +1,3 @@ -/// - import angular from 'angular'; import _ from 'lodash'; import $ from 'jquery'; @@ -35,12 +33,6 @@ export class AnnotationsEditorCtrl { this.datasources = datasourceSrv.getAnnotationSources(); this.annotations = $scope.dashboard.annotations.list; this.reset(); - - $scope.$watch('mode', newVal => { - if (newVal === 'new') { - this.reset(); - } - }); } datasourceChanged() { @@ -71,6 +63,11 @@ export class AnnotationsEditorCtrl { this.$scope.broadcastRefresh(); } + setupNew() { + this.mode = 'new'; + this.reset(); + } + add() { this.annotations.push(this.currentAnnotation); this.reset(); @@ -85,6 +82,14 @@ export class AnnotationsEditorCtrl { this.$scope.dashboard.updateSubmenuVisibility(); this.$scope.broadcastRefresh(); } + + annotationEnabledChange() { + this.$scope.broadcastRefresh(); + } + + annotationHiddenChanged() { + this.$scope.dashboard.updateSubmenuVisibility(); + } } coreModule.controller('AnnotationsEditorCtrl', AnnotationsEditorCtrl); diff --git a/public/app/features/annotations/event.ts b/public/app/features/annotations/event.ts index 53afbea5b07..24d0edbe1a2 100644 --- a/public/app/features/annotations/event.ts +++ b/public/app/features/annotations/event.ts @@ -2,9 +2,11 @@ export class AnnotationEvent { dashboardId: number; panelId: number; + userId: number; time: any; timeEnd: any; isRegion: boolean; - title: string; text: string; + type: string; + tags: string; } diff --git a/public/app/features/annotations/event_editor.ts b/public/app/features/annotations/event_editor.ts index e5311ef8c76..b8e0a40a7bd 100644 --- a/public/app/features/annotations/event_editor.ts +++ b/public/app/features/annotations/event_editor.ts @@ -1,6 +1,5 @@ -/// - import _ from 'lodash'; +import moment from 'moment'; import {coreModule} from 'app/core/core'; import {MetricsPanelCtrl} from 'app/plugins/sdk'; import {AnnotationEvent} from './event'; @@ -11,11 +10,20 @@ export class EventEditorCtrl { timeRange: {from: number, to: number}; form: any; close: any; + timeFormated: string; /** @ngInject **/ constructor(private annotationsSrv) { this.event.panelId = this.panelCtrl.panel.id; this.event.dashboardId = this.panelCtrl.dashboard.id; + + // Annotations query returns time as Unix timestamp in milliseconds + this.event.time = tryEpochToMoment(this.event.time); + if (this.event.isRegion) { + this.event.timeEnd = tryEpochToMoment(this.event.timeEnd); + } + + this.timeFormated = this.panelCtrl.dashboard.formatDate(this.event.time); } save() { @@ -28,7 +36,7 @@ export class EventEditorCtrl { saveModel.timeEnd = 0; if (saveModel.isRegion) { - saveModel.timeEnd = saveModel.timeEnd.valueOf(); + saveModel.timeEnd = this.event.timeEnd.valueOf(); if (saveModel.timeEnd < saveModel.time) { console.log('invalid time'); @@ -36,14 +44,48 @@ export class EventEditorCtrl { } } - this.annotationsSrv.saveAnnotationEvent(saveModel).then(() => { + if (saveModel.id) { + this.annotationsSrv.updateAnnotationEvent(saveModel) + .then(() => { + this.panelCtrl.refresh(); + this.close(); + }) + .catch(() => { + this.panelCtrl.refresh(); + this.close(); + }); + } else { + this.annotationsSrv.saveAnnotationEvent(saveModel) + .then(() => { + this.panelCtrl.refresh(); + this.close(); + }) + .catch(() => { + this.panelCtrl.refresh(); + this.close(); + }); + } + } + + delete() { + return this.annotationsSrv.deleteAnnotationEvent(this.event) + .then(() => { + this.panelCtrl.refresh(); + this.close(); + }) + .catch(() => { this.panelCtrl.refresh(); this.close(); }); } +} - timeChanged() { - this.panelCtrl.render(); +function tryEpochToMoment(timestamp) { + if (timestamp && _.isNumber(timestamp)) { + let 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 6b8a58f0b57..caa367e42f1 100644 --- a/public/app/features/annotations/event_manager.ts +++ b/public/app/features/annotations/event_manager.ts @@ -3,25 +3,30 @@ import moment from 'moment'; import {MetricsPanelCtrl} from 'app/plugins/sdk'; import {AnnotationEvent} from './event'; +const OK_COLOR = "rgba(11, 237, 50, 1)", + ALERTING_COLOR = "rgba(237, 46, 24, 1)", + NO_DATA_COLOR = "rgba(150, 150, 150, 1)"; + + export class EventManager { event: AnnotationEvent; + editorOpen: boolean; - constructor(private panelCtrl: MetricsPanelCtrl, private elem, private popoverSrv) { + constructor(private panelCtrl: MetricsPanelCtrl) { } editorClosed() { - console.log('editorClosed'); this.event = null; + this.editorOpen = false; this.panelCtrl.render(); } - updateTime(range) { - let newEvent = true; + editorOpened() { + this.editorOpen = true; + } - if (this.event) { - newEvent = false; - } else { - // init new event + updateTime(range) { + if (!this.event) { this.event = new AnnotationEvent(); this.event.dashboardId = this.panelCtrl.dashboard.id; this.event.panelId = this.panelCtrl.panel.id; @@ -35,25 +40,11 @@ export class EventManager { this.event.isRegion = true; } - // newEvent means the editor is not visible - if (!newEvent) { - this.panelCtrl.render(); - return; - } - - this.popoverSrv.show({ - element: this.elem[0], - classNames: 'drop-popover drop-popover--form', - position: 'bottom center', - openOn: null, - template: '', - onClose: this.editorClosed.bind(this), - model: { - event: this.event, - panelCtrl: this.panelCtrl, - }, - }); + this.panelCtrl.render(); + } + editEvent(event, elem?) { + this.event = event; this.panelCtrl.render(); } @@ -64,35 +55,54 @@ export class EventManager { var types = { '$__alerting': { - color: 'rgba(237, 46, 24, 1)', + color: ALERTING_COLOR, position: 'BOTTOM', markerSize: 5, }, '$__ok': { - color: 'rgba(11, 237, 50, 1)', + color: OK_COLOR, position: 'BOTTOM', markerSize: 5, }, '$__no_data': { - color: 'rgba(150, 150, 150, 1)', + color: NO_DATA_COLOR, position: 'BOTTOM', markerSize: 5, }, }; if (this.event) { - annotations = [ - { - min: this.event.time.valueOf(), - title: this.event.title, - text: this.event.text, - eventType: '$__alerting', - } - ]; + if (this.event.isRegion) { + annotations = [ + { + isRegion: true, + min: this.event.time.valueOf(), + timeEnd: this.event.timeEnd.valueOf(), + text: this.event.text, + eventType: '$__alerting', + editModel: this.event, + } + ]; + } else { + annotations = [ + { + min: this.event.time.valueOf(), + text: this.event.text, + editModel: this.event, + eventType: '$__alerting', + } + ]; + } } else { // annotations from query for (var i = 0; i < annotations.length; i++) { var item = annotations[i]; + + // add properties used by jquery flot events + item.min = item.time; + item.max = item.time; + item.eventType = item.source.name; + if (item.newState) { item.eventType = '$__' + item.newState; continue; @@ -108,10 +118,69 @@ export class EventManager { } } + let regions = getRegions(annotations); + addRegionMarking(regions, flotOptions); + + let eventSectionHeight = 20; + let eventSectionMargin = 7; + flotOptions.grid.eventSectionHeight = eventSectionMargin; + flotOptions.xaxis.eventSectionHeight = eventSectionHeight; + flotOptions.events = { levels: _.keys(types).length + 1, data: annotations, types: types, + manager: this }; } } + +function getRegions(events) { + return _.filter(events, 'isRegion'); +} + +function addRegionMarking(regions, flotOptions) { + let markings = flotOptions.grid.markings; + let defaultColor = 'rgb(237, 46, 24)'; + let fillColor; + + _.each(regions, region => { + if (region.source) { + fillColor = region.source.iconColor || defaultColor; + } else { + fillColor = defaultColor; + } + + // Convert #FFFFFF to rgb(255, 255, 255) + // because panels with alerting use this format + let hexPattern = /^#[\da-fA-f]{3,6}/; + if (hexPattern.test(fillColor)) { + fillColor = convertToRGB(fillColor); + } + + fillColor = addAlphaToRGB(fillColor, 0.090); + markings.push({ xaxis: { from: region.min, to: region.timeEnd }, color: fillColor }); + }); +} + +function addAlphaToRGB(rgb: string, alpha: number): string { + let rgbPattern = /^rgb\(/; + if (rgbPattern.test(rgb)) { + return rgb.replace(')', `, ${alpha})`).replace('rgb', 'rgba'); + } else { + return rgb.replace(/[\d\.]+\)/, `${alpha})`); + } +} + +function convertToRGB(hex: string): string { + let hexPattern = /#([\da-fA-F]{2})([\da-fA-F]{2})([\da-fA-F]{2})/g; + let match = hexPattern.exec(hex); + if (match) { + let rgb = _.map(match.slice(1), hex_val => { + return parseInt(hex_val, 16); + }); + return 'rgb(' + rgb.join(',') + ')'; + } else { + return ""; + } +} diff --git a/public/app/features/annotations/partials/editor.html b/public/app/features/annotations/partials/editor.html index 1506e1a0dc5..e4d9c2c413f 100644 --- a/public/app/features/annotations/partials/editor.html +++ b/public/app/features/annotations/partials/editor.html @@ -40,10 +40,11 @@ Annotations provide a way to integrate event data into your graphs. They are visualized as vertical lines and icons on all graph panels. When you hover over an annotation icon you can get title, tags, and text information for the event. In the Queries tab you can add queries that return annotation events. -
      -
      - Checkout the Annotations documentation for more information.

      +

      + You can add annotations directly from grafana by holding CTRL or CMD + click on graph (or drag region). These will be stored in Grafana's annotation database. +

      + Checkout the Annotations documentation for more information.
      @@ -53,13 +54,16 @@
      - + - @@ -77,60 +81,63 @@
      -
      -
      Options
      +
      +
      +
      General
      - Name - + Name +
      - Data source -
      + Data source +
      -
      -
      - - - - - - - - -
      +
      + +
      +
      + + + +
      - +
      +
      -
      Query
      - - - - +
      Query
      + + + + -
      -
      - - -
      +
      +
      + +
      +
      -
      diff --git a/public/app/features/annotations/partials/event_editor.html b/public/app/features/annotations/partials/event_editor.html index 6e44b6f768d..529434755f1 100644 --- a/public/app/features/annotations/partials/event_editor.html +++ b/public/app/features/annotations/partials/event_editor.html @@ -1,38 +1,35 @@ -
      Add annotation
      - - -
      -
      - Title - +
      +
      +
      - -
      -
      - Time - -
      -
      - -
      -
      - Start - -
      -
      - End - -
      -
      -
      - Description - -
      -
      - - Cancel -
      -
      - +
      + Add Annotation + Edit Annotation +
      + +
      {{ctrl.timeFormated}}
      +
      + +
      +
      +
      + Description + +
      + +
      + Tags + + +
      + +
      + + + Cancel +
      +
      + +
      diff --git a/public/app/features/annotations/specs/annotations_srv_specs.ts b/public/app/features/annotations/specs/annotations_srv_specs.ts new file mode 100644 index 00000000000..3c0142ed87f --- /dev/null +++ b/public/app/features/annotations/specs/annotations_srv_specs.ts @@ -0,0 +1,40 @@ +import {describe, beforeEach, it, expect, angularMocks} from 'test/lib/common'; +import '../annotations_srv'; +import helpers from 'test/specs/helpers'; + +describe('AnnotationsSrv', function() { + var ctx = new helpers.ServiceTestContext(); + + beforeEach(angularMocks.module('grafana.core')); + beforeEach(angularMocks.module('grafana.services')); + beforeEach(() => { + ctx.createService('annotationsSrv'); + }); + describe('When translating the query result', () => { + const annotationSource = { + datasource: '-- Grafana --', + enable: true, + hide: false, + limit: 200, + name: 'test', + scope: 'global', + tags: [ + 'test' + ], + type: 'event', + }; + + const time = 1507039543000; + const annotations = [{id: 1, panelId: 1, text: 'text', time: time}]; + let translatedAnnotations; + + beforeEach(() => { + translatedAnnotations = ctx.service.translateQueryResult(annotationSource, annotations); + }); + + it('should set defaults', () => { + expect(translatedAnnotations[0].source).to.eql(annotationSource); + }); + }); +}); + diff --git a/public/app/features/dashboard/model.ts b/public/app/features/dashboard/model.ts index 3eb3d25f4fe..11067d3c68b 100644 --- a/public/app/features/dashboard/model.ts +++ b/public/app/features/dashboard/model.ts @@ -71,10 +71,35 @@ export class DashboardModel { } } + this.addBuiltInAnnotationQuery(); this.updateSchema(data); this.initMeta(meta); } + addBuiltInAnnotationQuery() { + let found = false; + for (let item of this.annotations.list) { + if (item.builtIn === 1) { + found = true; + break; + } + } + + if (found) { + return; + } + + this.annotations.list.unshift({ + datasource: '-- Grafana --', + name: 'Annotations & Alerts', + type: 'dashboard', + iconColor: 'rgb(0, 211, 255)', + enable: true, + hide: true, + builtIn: 1, + }); + } + private initMeta(meta) { meta = meta || {}; diff --git a/public/app/features/dashboard/specs/dashboard_model_specs.ts b/public/app/features/dashboard/specs/dashboard_model_specs.ts index 6ca84ba89f3..ca5482bbfc5 100644 --- a/public/app/features/dashboard/specs/dashboard_model_specs.ts +++ b/public/app/features/dashboard/specs/dashboard_model_specs.ts @@ -46,8 +46,8 @@ describe('DashboardModel', function() { var saveModel = model.getSaveModelClone(); var keys = _.keys(saveModel); - expect(keys[0]).to.be('addEmptyRow'); - expect(keys[1]).to.be('addPanel'); + expect(keys[0]).to.be('addBuiltInAnnotationQuery'); + expect(keys[1]).to.be('addEmptyRow'); }); }); @@ -220,26 +220,6 @@ describe('DashboardModel', function() { }); }); - describe('when creating dashboard model with missing list for annoations or templating', function() { - var model; - - beforeEach(function() { - model = new DashboardModel({ - annotations: { - enable: true, - }, - templating: { - enable: true - } - }); - }); - - it('should add empty list', function() { - expect(model.annotations.list.length).to.be(0); - expect(model.templating.list.length).to.be(0); - }); - }); - describe('Given editable false dashboard', function() { var model; @@ -339,7 +319,12 @@ describe('DashboardModel', function() { }); it('should add empty list', function() { - expect(model.annotations.list.length).to.be(0); + expect(model.annotations.list.length).to.be(1); + expect(model.templating.list.length).to.be(0); + }); + + it('should add builtin annotation query', function() { + expect(model.annotations.list[0].builtIn).to.be(1); expect(model.templating.list.length).to.be(0); }); }); diff --git a/public/app/features/dashboard/specs/exporter_specs.ts b/public/app/features/dashboard/specs/exporter_specs.ts index 9364cea8c47..cc2b1ddaf97 100644 --- a/public/app/features/dashboard/specs/exporter_specs.ts +++ b/public/app/features/dashboard/specs/exporter_specs.ts @@ -80,6 +80,10 @@ describe('given dashboard with repeated panels', function() { name: 'mixed', meta: {id: "mixed", info: {version: "1.2.1"}, name: "Mixed", builtIn: true} })); + datasourceSrvStub.get.withArgs('-- Grafana --').returns(Promise.resolve({ + name: '-- Grafana --', + meta: {id: "grafana", info: {version: "1.2.1"}, name: "grafana", builtIn: true} + })); config.panels['graph'] = { id: "graph", @@ -116,7 +120,7 @@ describe('given dashboard with repeated panels', function() { }); it('should replace datasource in annotation query', function() { - expect(exported.annotations.list[0].datasource).to.be("${DS_GFDB}"); + expect(exported.annotations.list[1].datasource).to.be("${DS_GFDB}"); }); it('should add datasource as input', function() { diff --git a/public/app/features/dashboard/submenu/submenu.html b/public/app/features/dashboard/submenu/submenu.html index ce3c61f1cc3..9e9d22fb495 100644 --- a/public/app/features/dashboard/submenu/submenu.html +++ b/public/app/features/dashboard/submenu/submenu.html @@ -12,7 +12,7 @@
      diff --git a/public/app/features/org/prefs_control.ts b/public/app/features/org/prefs_control.ts index 07b277680ad..84cb2186f75 100644 --- a/public/app/features/org/prefs_control.ts +++ b/public/app/features/org/prefs_control.ts @@ -59,9 +59,13 @@ var template = `
      - Home Dashboard - + + Home Dashboard + + Not finding dashboard you want? Star it first, then it should appear in this select box. + + +
      diff --git a/public/app/headers/common.d.ts b/public/app/headers/common.d.ts index 94ae1b1abef..8b9dfa256ef 100644 --- a/public/app/headers/common.d.ts +++ b/public/app/headers/common.d.ts @@ -4,9 +4,3 @@ declare module 'eventemitter3' { var config: any; export default config; } - -declare module 'd3' { - var d3: any; - export default d3; -} - diff --git a/public/app/plugins/datasource/elasticsearch/datasource.ts b/public/app/plugins/datasource/elasticsearch/datasource.ts index 543e73d64e5..b165ad4c689 100644 --- a/public/app/plugins/datasource/elasticsearch/datasource.ts +++ b/public/app/plugins/datasource/elasticsearch/datasource.ts @@ -83,7 +83,6 @@ export class ElasticDatasource { var timeField = annotation.timeField || '@timestamp'; var queryString = annotation.query || '*'; var tagsField = annotation.tagsField || 'tags'; - var titleField = annotation.titleField || 'desc'; var textField = annotation.textField || null; var range = {}; @@ -146,9 +145,6 @@ export class ElasticDatasource { } } - if (_.isArray(fieldValue)) { - fieldValue = fieldValue.join(', '); - } return fieldValue; }; @@ -165,16 +161,27 @@ export class ElasticDatasource { var event = { annotation: annotation, time: moment.utc(time).valueOf(), - title: getFieldFromSource(source, titleField), + text: getFieldFromSource(source, textField), tags: getFieldFromSource(source, tagsField), - text: getFieldFromSource(source, textField) }; + // legacy support for title tield + if (annotation.titleField) { + const title = getFieldFromSource(source, annotation.titleField); + if (title) { + event.text = title + '\n' + event.text; + } + } + + if (typeof event.tags === 'string') { + event.tags = event.tags.split(','); + } + list.push(event); } return list; }); - }; + } testDatasource() { this.timeSrv.setTime({ from: 'now-1m', to: 'now' }, true); @@ -242,7 +249,7 @@ export class ElasticDatasource { return this.post('_msearch', payload).then(function(res) { return new ElasticResponse(sentTargets, res).getTimeSeries(); }); - }; + } getFields(query) { return this.get('/_mapping').then(function(result) { diff --git a/public/app/plugins/datasource/elasticsearch/index_pattern.ts b/public/app/plugins/datasource/elasticsearch/index_pattern.ts index 075c05dbf3f..22f121d02de 100644 --- a/public/app/plugins/datasource/elasticsearch/index_pattern.ts +++ b/public/app/plugins/datasource/elasticsearch/index_pattern.ts @@ -18,7 +18,7 @@ export class IndexPattern { } else { return this.pattern; } - }; + } getIndexList(from, to) { if (!this.interval) { diff --git a/public/app/plugins/datasource/elasticsearch/partials/annotations.editor.html b/public/app/plugins/datasource/elasticsearch/partials/annotations.editor.html index ad68312b727..d4e1e7d1b1c 100644 --- a/public/app/plugins/datasource/elasticsearch/partials/annotations.editor.html +++ b/public/app/plugins/datasource/elasticsearch/partials/annotations.editor.html @@ -15,24 +15,20 @@
      Field mappings
      - Time - + Time +
      -
      - Title + Text + +
      +
      + Tags + +
      +
      + Title (depricated)
      -
      -
      - Tags - -
      - -
      - Text - -
      -
      diff --git a/public/app/plugins/datasource/elasticsearch/query_builder.ts b/public/app/plugins/datasource/elasticsearch/query_builder.ts index ba4e1e436d4..754c541b2a8 100644 --- a/public/app/plugins/datasource/elasticsearch/query_builder.ts +++ b/public/app/plugins/datasource/elasticsearch/query_builder.ts @@ -167,7 +167,7 @@ export class ElasticQueryBuilder { break; } } - }; + } build(target, adhocFilters?, queryString?) { // make sure query has defaults; diff --git a/public/app/plugins/datasource/elasticsearch/query_def.ts b/public/app/plugins/datasource/elasticsearch/query_def.ts index dac48acae48..3cb63340b84 100644 --- a/public/app/plugins/datasource/elasticsearch/query_def.ts +++ b/public/app/plugins/datasource/elasticsearch/query_def.ts @@ -188,4 +188,4 @@ export function describeOrderBy(orderBy, target) { } else { return "metric not found"; } -}; +} diff --git a/public/app/plugins/datasource/grafana/datasource.ts b/public/app/plugins/datasource/grafana/datasource.ts index 2960af6b062..5ca3c433476 100644 --- a/public/app/plugins/datasource/grafana/datasource.ts +++ b/public/app/plugins/datasource/grafana/datasource.ts @@ -1,5 +1,3 @@ -/// - import _ from 'lodash'; class GrafanaDatasource { @@ -8,42 +6,62 @@ class GrafanaDatasource { constructor(private backendSrv, private $q) {} query(options) { - return this.backendSrv.get('/api/tsdb/testdata/random-walk', { - from: options.range.from.valueOf(), - to: options.range.to.valueOf(), - intervalMs: options.intervalMs, - maxDataPoints: options.maxDataPoints, - }).then(res => { - var data = []; + return this.backendSrv + .get('/api/tsdb/testdata/random-walk', { + from: options.range.from.valueOf(), + to: options.range.to.valueOf(), + intervalMs: options.intervalMs, + maxDataPoints: options.maxDataPoints, + }) + .then(res => { + var data = []; - if (res.results) { - _.forEach(res.results, queryRes => { - for (let series of queryRes.series) { - data.push({ - target: series.name, - datapoints: series.points - }); - } - }); - } + if (res.results) { + _.forEach(res.results, queryRes => { + for (let series of queryRes.series) { + data.push({ + target: series.name, + datapoints: series.points, + }); + } + }); + } - return {data: data}; - }); + return {data: data}; + }); } metricFindQuery(options) { return this.$q.when({data: []}); } + annotationQuery(options) { - return this.backendSrv.get('/api/annotations', { + const params: any = { from: options.range.from.valueOf(), to: options.range.to.valueOf(), - limit: options.limit, - type: options.type, - }); - } + limit: options.annotation.limit, + tags: options.annotation.tags, + }; + if (options.annotation.type === 'dashboard') { + // if no dashboard id yet return + if (!options.dashboard.id) { + return this.$q.when([]); + } + // filter by dashboard id + params.dashboardId = options.dashboard.id; + // remove tags filter if any + delete params.tags; + } else { + // require at least one tag + if (!_.isArray(options.annotation.tags) || options.annotation.tags.length === 0) { + return this.$q.when([]); + } + } + + return this.backendSrv.get('/api/annotations', params); + } } export {GrafanaDatasource}; diff --git a/public/app/plugins/datasource/grafana/module.ts b/public/app/plugins/datasource/grafana/module.ts index d03914bda19..eb0b582bfb1 100644 --- a/public/app/plugins/datasource/grafana/module.ts +++ b/public/app/plugins/datasource/grafana/module.ts @@ -1,5 +1,3 @@ -/// - import {GrafanaDatasource} from './datasource'; import {QueryCtrl} from 'app/plugins/sdk'; @@ -10,19 +8,22 @@ class GrafanaQueryCtrl extends QueryCtrl { class GrafanaAnnotationsQueryCtrl { annotation: any; + types = [ + {text: 'Dashboard', value: 'dashboard'}, + {text: 'Tags', value: 'tags'} + ]; + constructor() { - this.annotation.type = this.annotation.type || 'alert'; + this.annotation.type = this.annotation.type || 'tags'; this.annotation.limit = this.annotation.limit || 100; } static templateUrl = 'partials/annotations.editor.html'; } - export { GrafanaDatasource, GrafanaDatasource as Datasource, GrafanaQueryCtrl as QueryCtrl, GrafanaAnnotationsQueryCtrl as AnnotationsQueryCtrl, }; - diff --git a/public/app/plugins/datasource/grafana/partials/annotations.editor.html b/public/app/plugins/datasource/grafana/partials/annotations.editor.html index 24a06a2abd6..9803f082a23 100644 --- a/public/app/plugins/datasource/grafana/partials/annotations.editor.html +++ b/public/app/plugins/datasource/grafana/partials/annotations.editor.html @@ -2,14 +2,29 @@
      - Type -
      -
      + +
      + Tags + + +
      +
      - Max limit + Max limit
      @@ -17,3 +32,5 @@
      + + diff --git a/public/app/plugins/datasource/graphite/datasource.ts b/public/app/plugins/datasource/graphite/datasource.ts index 2c846ee6be8..5114922f1f7 100644 --- a/public/app/plugins/datasource/graphite/datasource.ts +++ b/public/app/plugins/datasource/graphite/datasource.ts @@ -68,6 +68,18 @@ export function GraphiteDatasource(instanceSettings, $q, backendSrv, templateSrv return result; }; + this.parseTags = function(tagString) { + let tags = []; + tags = tagString.split(','); + if (tags.length === 1) { + tags = tagString.split(' '); + if (tags[0] === '') { + tags = []; + } + } + return tags; + }; + this.annotationQuery = function(options) { // Graphite metric as annotation if (options.annotation.target) { @@ -102,19 +114,25 @@ export function GraphiteDatasource(instanceSettings, $q, backendSrv, templateSrv } else { // Graphite event as annotation var tags = templateSrv.replace(options.annotation.tags); - return this.events({range: options.rangeRaw, tags: tags}).then(function(results) { + return this.events({range: options.rangeRaw, tags: tags}).then(results => { var list = []; for (var i = 0; i < results.data.length; i++) { var e = results.data[i]; + var tags = e.tags; + if (_.isString(e.tags)) { + tags = this.parseTags(e.tags); + } + list.push({ annotation: options.annotation, time: e.when * 1000, title: e.what, - tags: e.tags, + tags: tags, text: e.data }); } + return list; }); } @@ -126,7 +144,6 @@ export function GraphiteDatasource(instanceSettings, $q, backendSrv, templateSrv if (options.tags) { tags = '&tags=' + options.tags; } - return this.doGraphiteRequest({ method: 'GET', url: '/events/get_data?from=' + this.translateTime(options.range.from, false) + diff --git a/public/app/plugins/datasource/graphite/specs/datasource_specs.ts b/public/app/plugins/datasource/graphite/specs/datasource_specs.ts index f25bbbd8910..182ba475331 100644 --- a/public/app/plugins/datasource/graphite/specs/datasource_specs.ts +++ b/public/app/plugins/datasource/graphite/specs/datasource_specs.ts @@ -2,10 +2,12 @@ import {describe, beforeEach, it, expect, angularMocks} from 'test/lib/common'; import helpers from 'test/specs/helpers'; import {GraphiteDatasource} from "../datasource"; +import moment from 'moment'; +import _ from 'lodash'; describe('graphiteDatasource', function() { - var ctx = new helpers.ServiceTestContext(); - var instanceSettings: any = {url: [''], name: 'graphiteProd', jsonData: {}}; + let ctx = new helpers.ServiceTestContext(); + let instanceSettings: any = {url: [''], name: 'graphiteProd', jsonData: {}}; beforeEach(angularMocks.module('grafana.core')); beforeEach(angularMocks.module('grafana.services')); @@ -22,16 +24,16 @@ describe('graphiteDatasource', function() { ctx.ds = ctx.$injector.instantiate(GraphiteDatasource, {instanceSettings: instanceSettings}); }); - describe('When querying influxdb with one target using query editor target spec', function() { - var query = { + describe('When querying graphite with one target using query editor target spec', function() { + let query = { panelId: 3, rangeRaw: { from: 'now-1h', to: 'now' }, targets: [{ target: 'prod1.count' }, {target: 'prod2.count'}], maxDataPoints: 500, }; - var results; - var requestOptions; + let results; + let requestOptions; beforeEach(function() { ctx.backendSrv.datasourceRequest = function(options) { @@ -52,7 +54,7 @@ describe('graphiteDatasource', function() { }); it('should query correctly', function() { - var params = requestOptions.data.split('&'); + let params = requestOptions.data.split('&'); expect(params).to.contain('target=prod1.count'); expect(params).to.contain('target=prod2.count'); expect(params).to.contain('from=-1h'); @@ -60,7 +62,7 @@ describe('graphiteDatasource', function() { }); it('should exclude undefined params', function() { - var params = requestOptions.data.split('&'); + let params = requestOptions.data.split('&'); expect(params).to.not.contain('cacheTimeout=undefined'); }); @@ -75,58 +77,130 @@ describe('graphiteDatasource', function() { }); + describe('when fetching Graphite Events as annotations', () => { + let results; + + const options = { + annotation: { + tags: 'tag1' + }, + range: { + from: moment(1432288354), + to: moment(1432288401) + }, + rangeRaw: {from: "now-24h", to: "now"} + }; + + describe('and tags are returned as string', () => { + const response = { + data: [ + { + when: 1507222850, + tags: 'tag1 tag2', + data: 'some text', + id: 2, + what: 'Event - deploy' + } + ]}; + + beforeEach(() => { + ctx.backendSrv.datasourceRequest = function(options) { + return ctx.$q.when(response); + }; + + ctx.ds.annotationQuery(options).then(function(data) { results = data; }); + ctx.$rootScope.$apply(); + }); + + it('should parse the tags string into an array', () => { + expect(_.isArray(results[0].tags)).to.eql(true); + expect(results[0].tags.length).to.eql(2); + expect(results[0].tags[0]).to.eql('tag1'); + expect(results[0].tags[1]).to.eql('tag2'); + }); + }); + + describe('and tags are returned as an array', () => { + const response = { + data: [ + { + when: 1507222850, + tags: ['tag1', 'tag2'], + data: 'some text', + id: 2, + what: 'Event - deploy' + } + ]}; + beforeEach(() => { + ctx.backendSrv.datasourceRequest = function(options) { + return ctx.$q.when(response); + }; + + ctx.ds.annotationQuery(options).then(function(data) { results = data; }); + ctx.$rootScope.$apply(); + }); + + it('should parse the tags string into an array', () => { + expect(_.isArray(results[0].tags)).to.eql(true); + expect(results[0].tags.length).to.eql(2); + expect(results[0].tags[0]).to.eql('tag1'); + expect(results[0].tags[1]).to.eql('tag2'); + }); + }); + }); + describe('building graphite params', function() { it('should return empty array if no targets', function() { - var results = ctx.ds.buildGraphiteParams({ + let results = ctx.ds.buildGraphiteParams({ targets: [{}] }); expect(results.length).to.be(0); }); it('should uri escape targets', function() { - var results = ctx.ds.buildGraphiteParams({ + let results = ctx.ds.buildGraphiteParams({ targets: [{target: 'prod1.{test,test2}'}, {target: 'prod2.count'}] }); expect(results).to.contain('target=prod1.%7Btest%2Ctest2%7D'); }); it('should replace target placeholder', function() { - var results = ctx.ds.buildGraphiteParams({ + let results = ctx.ds.buildGraphiteParams({ targets: [{target: 'series1'}, {target: 'series2'}, {target: 'asPercent(#A,#B)'}] }); expect(results[2]).to.be('target=asPercent(series1%2Cseries2)'); }); it('should replace target placeholder for hidden series', function() { - var results = ctx.ds.buildGraphiteParams({ + let results = ctx.ds.buildGraphiteParams({ targets: [{target: 'series1', hide: true}, {target: 'sumSeries(#A)', hide: true}, {target: 'asPercent(#A,#B)'}] }); expect(results[0]).to.be('target=' + encodeURIComponent('asPercent(series1,sumSeries(series1))')); }); it('should replace target placeholder when nesting query references', function() { - var results = ctx.ds.buildGraphiteParams({ + let results = ctx.ds.buildGraphiteParams({ targets: [{target: 'series1'}, {target: 'sumSeries(#A)'}, {target: 'asPercent(#A,#B)'}] }); expect(results[2]).to.be('target=' + encodeURIComponent("asPercent(series1,sumSeries(series1))")); }); it('should fix wrong minute interval parameters', function() { - var results = ctx.ds.buildGraphiteParams({ + let results = ctx.ds.buildGraphiteParams({ targets: [{target: "summarize(prod.25m.count, '25m', 'sum')" }] }); expect(results[0]).to.be('target=' + encodeURIComponent("summarize(prod.25m.count, '25min', 'sum')")); }); it('should fix wrong month interval parameters', function() { - var results = ctx.ds.buildGraphiteParams({ + let results = ctx.ds.buildGraphiteParams({ targets: [{target: "summarize(prod.5M.count, '5M', 'sum')" }] }); expect(results[0]).to.be('target=' + encodeURIComponent("summarize(prod.5M.count, '5mon', 'sum')")); }); it('should ignore empty targets', function() { - var results = ctx.ds.buildGraphiteParams({ + let results = ctx.ds.buildGraphiteParams({ targets: [{target: 'series1'}, {target: ''}] }); expect(results.length).to.be(2); diff --git a/public/app/plugins/datasource/influxdb/datasource.ts b/public/app/plugins/datasource/influxdb/datasource.ts index a29e520c159..c0aa246c093 100644 --- a/public/app/plugins/datasource/influxdb/datasource.ts +++ b/public/app/plugins/datasource/influxdb/datasource.ts @@ -1,5 +1,3 @@ -/// - import _ from 'lodash'; import * as dateMath from 'app/core/utils/datemath'; diff --git a/public/app/plugins/datasource/influxdb/partials/annotations.editor.html b/public/app/plugins/datasource/influxdb/partials/annotations.editor.html index da8f4edf881..475eaa2d4e0 100644 --- a/public/app/plugins/datasource/influxdb/partials/annotations.editor.html +++ b/public/app/plugins/datasource/influxdb/partials/annotations.editor.html @@ -9,18 +9,16 @@
      - Title - + Text +
      -
      Tags
      - -
      - Text - +
      + Title (depricated) +
      diff --git a/public/app/plugins/datasource/mysql/module.ts b/public/app/plugins/datasource/mysql/module.ts index c32c832d6a0..2d953d941b5 100644 --- a/public/app/plugins/datasource/mysql/module.ts +++ b/public/app/plugins/datasource/mysql/module.ts @@ -9,7 +9,6 @@ class MysqlConfigCtrl { const defaultQuery = `SELECT UNIX_TIMESTAMP() as time_sec, - as title, as text, as tags FROM
      -   + +   {{annotation.name}} +   + {{annotation.name}} (Built-in) + @@ -67,7 +71,7 @@ - +
      diff --git a/public/app/plugins/datasource/mysql/response_parser.ts b/public/app/plugins/datasource/mysql/response_parser.ts index 5501e4fc17a..22ea20a2851 100644 --- a/public/app/plugins/datasource/mysql/response_parser.ts +++ b/public/app/plugins/datasource/mysql/response_parser.ts @@ -106,7 +106,6 @@ export default class ResponseParser { const table = data.data.results[options.annotation.name].tables[0]; let timeColumnIndex = -1; - let titleColumnIndex = -1; let textColumnIndex = -1; let tagsColumnIndex = -1; @@ -114,7 +113,7 @@ export default class ResponseParser { if (table.columns[i].text === 'time_sec') { timeColumnIndex = i; } else if (table.columns[i].text === 'title') { - titleColumnIndex = i; + return this.$q.reject({message: 'Title return column on annotations are depricated, return only a column named text'}); } else if (table.columns[i].text === 'text') { textColumnIndex = i; } else if (table.columns[i].text === 'tags') { @@ -132,7 +131,6 @@ export default class ResponseParser { list.push({ annotation: options.annotation, time: Math.floor(row[timeColumnIndex]) * 1000, - title: row[titleColumnIndex], text: row[textColumnIndex], tags: row[tagsColumnIndex] ? row[tagsColumnIndex].trim().split(/\s*,\s*/) : [] }); diff --git a/public/app/plugins/datasource/mysql/specs/datasource_specs.ts b/public/app/plugins/datasource/mysql/specs/datasource_specs.ts index 08d2f8922a5..989d3d59395 100644 --- a/public/app/plugins/datasource/mysql/specs/datasource_specs.ts +++ b/public/app/plugins/datasource/mysql/specs/datasource_specs.ts @@ -27,7 +27,7 @@ describe('MySQLDatasource', function() { const options = { annotation: { name: annotationName, - rawQuery: 'select time_sec, title, text, tags from table;' + rawQuery: 'select time_sec, text, tags from table;' }, range: { from: moment(1432288354), @@ -41,11 +41,11 @@ describe('MySQLDatasource', function() { refId: annotationName, tables: [ { - columns: [{text: 'time_sec'}, {text: 'title'}, {text: 'text'}, {text: 'tags'}], + columns: [{text: 'time_sec'}, {text: 'text'}, {text: 'tags'}], rows: [ - [1432288355, 'aTitle', 'some text', 'TagA,TagB'], - [1432288390, 'aTitle2', 'some text2', ' TagB , TagC'], - [1432288400, 'aTitle3', 'some text3'] + [1432288355, 'some text', 'TagA,TagB'], + [1432288390, 'some text2', ' TagB , TagC'], + [1432288400, 'some text3'] ] } ] @@ -64,7 +64,6 @@ describe('MySQLDatasource', function() { it('should return annotation list', function() { expect(results.length).to.be(3); - expect(results[0].title).to.be('aTitle'); expect(results[0].text).to.be('some text'); expect(results[0].tags[0]).to.be('TagA'); expect(results[0].tags[1]).to.be('TagB'); diff --git a/public/app/plugins/datasource/opentsdb/datasource.js b/public/app/plugins/datasource/opentsdb/datasource.js index 5228ce26b9c..4d51b117ed4 100644 --- a/public/app/plugins/datasource/opentsdb/datasource.js +++ b/public/app/plugins/datasource/opentsdb/datasource.js @@ -91,9 +91,8 @@ function (angular, _, dateMath) { if(annotationObject) { _.each(annotationObject, function(annotation) { var event = { - title: annotation.description, + text: annotation.description, time: Math.floor(annotation.startTime) * 1000, - text: annotation.notes, annotation: options.annotation }; diff --git a/public/app/plugins/panel/alertlist/module.html b/public/app/plugins/panel/alertlist/module.html index 39dbb5bbe26..a88c4ebadc7 100644 --- a/public/app/plugins/panel/alertlist/module.html +++ b/public/app/plugins/panel/alertlist/module.html @@ -33,7 +33,7 @@
      -

      {{al.title}}

      +

      {{al.alertName}}

      {{al.stateModel.text}} {{al.info}} diff --git a/public/app/plugins/panel/graph/graph.ts b/public/app/plugins/panel/graph/graph.ts index a203648ae04..723a92ad19b 100755 --- a/public/app/plugins/panel/graph/graph.ts +++ b/public/app/plugins/panel/graph/graph.ts @@ -22,7 +22,7 @@ import {EventManager} from 'app/features/annotations/all'; import {convertValuesToHistogram, getSeriesValues} from './histogram'; /** @ngInject **/ -function graphDirective($rootScope, timeSrv, popoverSrv) { +function graphDirective($rootScope, timeSrv, popoverSrv, contextSrv) { return { restrict: 'A', template: '', @@ -37,7 +37,7 @@ function graphDirective($rootScope, timeSrv, popoverSrv) { var legendSideLastValue = null; var rootScope = scope.$root; var panelWidth = 0; - var eventManager = new EventManager(ctrl, elem, popoverSrv); + var eventManager = new EventManager(ctrl); var thresholdManager = new ThresholdManager(ctrl); var tooltip = new GraphTooltip(elem, dashboard, scope, function() { return sortedSeries; @@ -268,6 +268,7 @@ function graphDirective($rootScope, timeSrv, popoverSrv) { clickable: true, color: '#c8c8c8', margin: { left: 0, right: 0 }, + labelMarginX: 0, }, selection: { mode: "x", @@ -651,10 +652,10 @@ function graphDirective($rootScope, timeSrv, popoverSrv) { } elem.bind("plotselected", function (event, ranges) { - if (ranges.ctrlKey || ranges.metaKey) { - // scope.$apply(() => { - // eventManager.updateTime(ranges.xaxis); - // }); + if ((ranges.ctrlKey || ranges.metaKey) && contextSrv.isEditor) { + setTimeout(() => { + eventManager.updateTime(ranges.xaxis); + }, 100); } else { scope.$apply(function() { timeSrv.setTime({ @@ -666,13 +667,13 @@ function graphDirective($rootScope, timeSrv, popoverSrv) { }); elem.bind("plotclick", function (event, pos, item) { - if (pos.ctrlKey || pos.metaKey || eventManager.event) { + if ((pos.ctrlKey || pos.metaKey) && contextSrv.isEditor) { // Skip if range selected (added in "plotselected" event handler) let isRangeSelection = pos.x !== pos.x1; if (!isRangeSelection) { - // scope.$apply(() => { - // eventManager.updateTime({from: pos.x, to: null}); - // }); + setTimeout(() => { + eventManager.updateTime({from: pos.x, to: null}); + }, 100); } } }); diff --git a/public/app/plugins/panel/graph/jquery.flot.events.js b/public/app/plugins/panel/graph/jquery.flot.events.js index 3fc3db0d6d3..1aa79c5056f 100644 --- a/public/app/plugins/panel/graph/jquery.flot.events.js +++ b/public/app/plugins/panel/graph/jquery.flot.events.js @@ -7,14 +7,18 @@ define([ function ($, _, angular, Drop) { 'use strict'; - function createAnnotationToolip(element, event) { + function createAnnotationToolip(element, event, plot) { var injector = angular.element(document).injector(); var content = document.createElement('div'); - content.innerHTML = ''; + content.innerHTML = ''; injector.invoke(["$compile", "$rootScope", function($compile, $rootScope) { + var eventManager = plot.getOptions().events.manager; var tmpScope = $rootScope.$new(true); tmpScope.event = event; + tmpScope.onEdit = function() { + eventManager.editEvent(event); + }; $compile(content)(tmpScope); tmpScope.$digest(); @@ -42,6 +46,69 @@ function ($, _, angular, Drop) { }]); } + var markerElementToAttachTo = null; + + function createEditPopover(element, event, plot) { + var 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) + markerElementToAttachTo = element; + return; + } + + // mark as openend + eventManager.editorOpened(); + // set marker elment to attache to + markerElementToAttachTo = element; + + // wait for element to be attached and positioned + setTimeout(function() { + + var injector = angular.element(document).injector(); + var content = document.createElement('div'); + content.innerHTML = ''; + + injector.invoke(["$compile", "$rootScope", function($compile, $rootScope) { + var scope = $rootScope.$new(true); + var drop; + + scope.event = event; + scope.panelCtrl = eventManager.panelCtrl; + scope.close = function() { + drop.close(); + }; + + $compile(content)(scope); + scope.$digest(); + + drop = new Drop({ + target: markerElementToAttachTo[0], + content: content, + position: "bottom center", + classes: 'drop-popover drop-popover--form', + openOn: 'click', + tetherOptions: { + constraints: [{to: 'window', pin: true, attachment: "both"}] + } + }); + + drop.open(); + eventManager.editorOpened(); + + drop.on('close', function() { + // need timeout here in order call drop.destroy + setTimeout(function() { + eventManager.editorClosed(); + scope.$destroy(); + drop.destroy(); + }); + }); + }]); + + }, 100); + } + /* * jquery.flot.events * @@ -121,11 +188,20 @@ function ($, _, angular, Drop) { */ this.setupEvents = function(events) { var that = this; + var parts = _.partition(events, 'isRegion'); + var regions = parts[0]; + events = parts[1]; + $.each(events, function(index, event) { var ve = new VisualEvent(event, that._buildDiv(event)); _events.push(ve); }); + $.each(regions, function (index, event) { + var vre = new VisualEvent(event, that._buildRegDiv(event)); + _events.push(vre); + }); + _events.sort(function(a, b) { var ao = a.getOptions(), bo = b.getOptions(); if (ao.min > bo.min) { return 1; } @@ -232,7 +308,10 @@ function ($, _, angular, Drop) { lineWidth = this._types[eventTypeId].lineWidth; } - top = o.top + this._plot.height(); + var topOffset = xaxis.options.eventSectionHeight || 0; + topOffset = topOffset / 3; + + top = o.top + this._plot.height() + topOffset; left = xaxis.p2c(event.min) + o.left; var line = $('
      ').css({ @@ -241,25 +320,27 @@ function ($, _, angular, Drop) { "left": left + 'px', "top": 8, "width": lineWidth + "px", - "height": this._plot.height(), + "height": this._plot.height() + topOffset * 0.8, "border-left-width": lineWidth + "px", "border-left-style": lineStyle, - "border-left-color": color + "border-left-color": color, + "color": color }) .appendTo(container); if (markerShow) { var marker = $('
      ').css({ "position": "absolute", - "left": (-markerSize-Math.round(lineWidth/2)) + "px", + "left": (-markerSize - Math.round(lineWidth / 2)) + "px", "font-size": 0, "line-height": 0, "width": 0, "height": 0, "border-left": markerSize+"px solid transparent", "border-right": markerSize+"px solid transparent" - }) - .appendTo(line); + }); + + marker.appendTo(line); if (this._types[eventTypeId] && this._types[eventTypeId].position && this._types[eventTypeId].position.toUpperCase() === 'BOTTOM') { marker.css({ @@ -280,9 +361,13 @@ function ($, _, angular, Drop) { }); var mouseenter = function() { - createAnnotationToolip(marker, $(this).data("event")); + createAnnotationToolip(marker, $(this).data("event"), that._plot); }; + if (event.editModel) { + createEditPopover(marker, event.editModel, that._plot); + } + var mouseleave = function() { that._plot.clearSelection(); }; @@ -312,6 +397,127 @@ function ($, _, angular, Drop) { return drawableEvent; }; + /** + * create a DOM element for the given region + */ + this._buildRegDiv = function (event) { + var that = this; + + var container = this._plot.getPlaceholder(); + var o = this._plot.getPlotOffset(); + var axes = this._plot.getAxes(); + var xaxis = this._plot.getXAxes()[this._plot.getOptions().events.xaxis - 1]; + var yaxis, top, left, lineWidth, regionWidth, lineStyle, color, markerTooltip; + + // determine the y axis used + if (axes.yaxis && axes.yaxis.used) { yaxis = axes.yaxis; } + if (axes.yaxis2 && axes.yaxis2.used) { yaxis = axes.yaxis2; } + + // map the eventType to a types object + var eventTypeId = event.eventType; + + if (this._types === null || !this._types[eventTypeId] || !this._types[eventTypeId].color) { + color = '#666'; + } else { + color = this._types[eventTypeId].color; + } + + if (this._types === null || !this._types[eventTypeId] || this._types[eventTypeId].markerTooltip === undefined) { + markerTooltip = true; + } else { + markerTooltip = this._types[eventTypeId].markerTooltip; + } + + if (this._types == null || !this._types[eventTypeId] || this._types[eventTypeId].lineWidth === undefined) { + lineWidth = 1; //default line width + } else { + lineWidth = this._types[eventTypeId].lineWidth; + } + + if (this._types == null || !this._types[eventTypeId] || !this._types[eventTypeId].lineStyle) { + lineStyle = 'dashed'; //default line style + } else { + lineStyle = this._types[eventTypeId].lineStyle.toLowerCase(); + } + + var topOffset = 2; + top = o.top + this._plot.height() + topOffset; + + var timeFrom = Math.min(event.min, event.timeEnd); + var timeTo = Math.max(event.min, event.timeEnd); + left = xaxis.p2c(timeFrom) + o.left; + var right = xaxis.p2c(timeTo) + o.left; + regionWidth = right - left; + + _.each([left, right], function(position) { + var line = $('
      ').css({ + "position": "absolute", + "opacity": 0.8, + "left": position + 'px', + "top": 8, + "width": lineWidth + "px", + "height": that._plot.height() + topOffset, + "border-left-width": lineWidth + "px", + "border-left-style": lineStyle, + "border-left-color": color, + "color": color + }); + line.appendTo(container); + }); + + var region = $('
      ').css({ + "position": "absolute", + "opacity": 0.5, + "left": left + 'px', + "top": top, + "width": Math.round(regionWidth + lineWidth) + "px", + "height": "0.5rem", + "border-left-color": color, + "color": color, + "background-color": color + }); + region.appendTo(container); + + region.data({ + "event": event + }); + + var mouseenter = function () { + createAnnotationToolip(region, $(this).data("event"), that._plot); + }; + + if (event.editModel) { + createEditPopover(region, event.editModel, that._plot); + } + + var mouseleave = function () { + that._plot.clearSelection(); + }; + + if (markerTooltip) { + region.css({ "cursor": "help" }); + region.hover(mouseenter, mouseleave); + } + + var drawableEvent = new DrawableEvent( + region, + function drawFunc(obj) { obj.show(); }, + function (obj) { obj.remove(); }, + function (obj, position) { + obj.css({ + top: position.top, + left: position.left + }); + }, + left, + top, + region.width(), + region.height() + ); + + return drawableEvent; + }; + /** * check if the event is inside visible range */ @@ -395,5 +601,4 @@ function ($, _, angular, Drop) { name: "events", version: "0.2.5" }); - }); diff --git a/public/app/system.conf.js b/public/app/system.conf.js new file mode 100644 index 00000000000..88ab2670e78 --- /dev/null +++ b/public/app/system.conf.js @@ -0,0 +1,83 @@ +System.config({ + defaultJSExtenions: true, + baseURL: 'public', + paths: { + 'virtual-scroll': 'vendor/npm/virtual-scroll/src/index.js', + 'mousetrap': 'vendor/npm/mousetrap/mousetrap.js', + 'remarkable': 'vendor/npm/remarkable/dist/remarkable.js', + 'tether': 'vendor/npm/tether/dist/js/tether.js', + 'eventemitter3': 'vendor/npm/eventemitter3/index.js', + 'tether-drop': 'vendor/npm/tether-drop/dist/js/drop.js', + 'moment': 'vendor/moment.js', + "jquery": "vendor/jquery/dist/jquery.js", + 'lodash-src': 'vendor/lodash/dist/lodash.js', + "lodash": 'app/core/lodash_extended.js', + "angular": "vendor/angular/angular.js", + "bootstrap": "vendor/bootstrap/bootstrap.js", + 'angular-route': 'vendor/angular-route/angular-route.js', + 'angular-sanitize': 'vendor/angular-sanitize/angular-sanitize.js', + "angular-ui": "vendor/angular-ui/ui-bootstrap-tpls.js", + "angular-strap": "vendor/angular-other/angular-strap.js", + "angular-dragdrop": "vendor/angular-native-dragdrop/draganddrop.js", + "angular-bindonce": "vendor/angular-bindonce/bindonce.js", + "spectrum": "vendor/spectrum.js", + "bootstrap-tagsinput": "vendor/tagsinput/bootstrap-tagsinput.js", + "jquery.flot": "vendor/flot/jquery.flot", + "jquery.flot.pie": "vendor/flot/jquery.flot.pie", + "jquery.flot.selection": "vendor/flot/jquery.flot.selection", + "jquery.flot.stack": "vendor/flot/jquery.flot.stack", + "jquery.flot.stackpercent": "vendor/flot/jquery.flot.stackpercent", + "jquery.flot.time": "vendor/flot/jquery.flot.time", + "jquery.flot.crosshair": "vendor/flot/jquery.flot.crosshair", + "jquery.flot.fillbelow": "vendor/flot/jquery.flot.fillbelow", + "jquery.flot.gauge": "vendor/flot/jquery.flot.gauge", + "d3": "vendor/d3/d3.js", + "jquery.flot.dashes": "vendor/flot/jquery.flot.dashes", + "twemoji": "vendor/npm/twemoji/2/twemoji.amd.js", + "ace": "vendor/npm/ace-builds/src-noconflict/ace", + }, + + packages: { + app: { + defaultExtension: 'js', + }, + vendor: { + defaultExtension: 'js', + }, + plugins: { + defaultExtension: 'js', + }, + test: { + defaultExtension: 'js', + }, + }, + + map: { + text: 'vendor/plugin-text/text.js', + css: 'app/core/utils/css_loader.js' + }, + + meta: { + 'vendor/npm/virtual-scroll/src/indx.js': { + format: 'cjs', + exports: 'VirtualScroll', + }, + 'vendor/angular/angular.js': { + format: 'global', + deps: ['jquery'], + exports: 'angular', + }, + 'vendor/npm/eventemitter3/index.js': { + format: 'cjs', + exports: 'EventEmitter' + }, + 'vendor/npm/mousetrap/mousetrap.js': { + format: 'global', + exports: 'Mousetrap' + }, + 'vendor/npm/ace-builds/src-noconflict/ace.js': { + format: 'global', + exports: 'ace' + } + } +}); diff --git a/public/sass/_grafana.scss b/public/sass/_grafana.scss index f2abb2b8b3f..79ee1799b90 100644 --- a/public/sass/_grafana.scss +++ b/public/sass/_grafana.scss @@ -78,6 +78,7 @@ @import "components/jsontree"; @import "components/edit_sidemenu.scss"; @import "components/row.scss"; +@import "components/icon-picker.scss"; @import "components/json_explorer.scss"; @import "components/code_editor.scss"; diff --git a/public/sass/_variables.dark.scss b/public/sass/_variables.dark.scss index 4de87a0aaf0..0dc00cf5a9c 100644 --- a/public/sass/_variables.dark.scss +++ b/public/sass/_variables.dark.scss @@ -251,7 +251,8 @@ $alert-info-bg: linear-gradient(100deg, #1a4552, #00374a); // popover $popover-bg: $panel-bg; $popover-color: $text-color; -$popover-border-color: $gray-1; +$popover-border-color: $dark-4; +$popover-shadow: 0 0 20px black; $popover-help-bg: $btn-secondary-bg; $popover-help-color: $text-color; diff --git a/public/sass/_variables.light.scss b/public/sass/_variables.light.scss index 533daec705b..e6901e1c772 100644 --- a/public/sass/_variables.light.scss +++ b/public/sass/_variables.light.scss @@ -270,9 +270,11 @@ $alert-warning-bg: linear-gradient(90deg, #d44939, #e0603d); $alert-info-bg: $blue-dark; // popover -$popover-bg: $gray-5; +$popover-bg: $panel-bg; $popover-color: $text-color; -$popover-border-color: $gray-3; +$popover-border-color: $gray-5; +$popover-shadow: 0 0 20px $white; + $popover-help-bg: $blue-dark; $popover-help-color: $gray-6; $popover-error-bg: $btn-danger-bg; diff --git a/public/sass/components/_drop.scss b/public/sass/components/_drop.scss index 9e3c884bc68..c1441bd31cb 100644 --- a/public/sass/components/_drop.scss +++ b/public/sass/components/_drop.scss @@ -51,9 +51,16 @@ $easing: cubic-bezier(0, 0, 0.265, 1.00); } } +.drop-element.drop-popover { + .drop-content { + box-shadow: $popover-shadow; + } +} + .drop-element.drop-popover--form { .drop-content { max-width: none; + padding: 0; } } diff --git a/public/sass/components/_icon-picker.scss b/public/sass/components/_icon-picker.scss new file mode 100644 index 00000000000..796f3f95db5 --- /dev/null +++ b/public/sass/components/_icon-picker.scss @@ -0,0 +1,26 @@ +.gf-icon-picker { + width: 400px; + height: 450px; + + .icon-filter { + padding-bottom: 10px; + margin: auto; + width: 50%; + } + + .icon-container { + max-height: 350px; + overflow: auto; + + .gf-event-icon { + margin: 0.4rem; + height: 1.5rem; + } + } +} + +.gf-icon-picker-button { + .gf-event-icon { + height: 1.2rem; + } +} diff --git a/public/sass/components/_panel_graph.scss b/public/sass/components/_panel_graph.scss index 5f9178df2f7..45372f92a65 100644 --- a/public/sass/components/_panel_graph.scss +++ b/public/sass/components/_panel_graph.scss @@ -287,19 +287,27 @@ margin-top: 8px; } - .graph-annotation-header { - background-color: $input-label-bg; + .graph-annotation__header { + background-color: $popover-border-color; padding: 0.40rem 0.65rem; + display: flex; } - .graph-annotation-title { + .graph-annotation__title { font-weight: $font-weight-semi-bold; padding-right: $spacer; - position: relative; - top: 2px; + overflow: hidden; + display: inline-block; + white-space: nowrap; + text-overflow: ellipsis; + flex-grow: 1; } - .graph-annotation-time { + .graph-annotation__edit-icon { + padding-left: $spacer; + } + + .graph-annotation__time { color: $text-muted; font-style: italic; font-weight: normal; @@ -308,15 +316,22 @@ top: 1px; } - .graph-annotation-body { + .graph-annotation__body { padding: 0.65rem; } - a { + .graph-annotation__user { + img { + border-radius: 50%; + width: 16px; + height: 16px; + } + } + + a[href] { color: $blue; text-decoration: underline; } - } .left-yaxis-label { diff --git a/public/sass/mixins/_drop_element.scss b/public/sass/mixins/_drop_element.scss index 7aa51fff256..f1bb69efd98 100644 --- a/public/sass/mixins/_drop_element.scss +++ b/public/sass/mixins/_drop_element.scss @@ -16,10 +16,6 @@ max-width: 20rem; border: 1px solid $border-color; - @if $theme-bg != $border-color { - box-shadow: 0 0 15px $border-color; - } - &:before { content: ""; display: block; diff --git a/public/test/test-main.js b/public/test/test-main.js new file mode 100644 index 00000000000..1347c421a64 --- /dev/null +++ b/public/test/test-main.js @@ -0,0 +1,130 @@ +(function() { + "use strict"; + + // Tun on full stack traces in errors to help debugging + Error.stackTraceLimit=Infinity; + + window.__karma__.loaded = function() {}; + + System.config({ + baseURL: '/base/', + defaultJSExtensions: true, + paths: { + 'mousetrap': 'vendor/npm/mousetrap/mousetrap.js', + 'eventemitter3': 'vendor/npm/eventemitter3/index.js', + 'remarkable': 'vendor/npm/remarkable/dist/remarkable.js', + 'tether': 'vendor/npm/tether/dist/js/tether.js', + 'tether-drop': 'vendor/npm/tether-drop/dist/js/drop.js', + 'moment': 'vendor/moment.js', + "jquery": "vendor/jquery/dist/jquery.js", + 'lodash-src': 'vendor/lodash/dist/lodash.js', + "lodash": 'app/core/lodash_extended.js', + "angular": 'vendor/angular/angular.js', + 'angular-mocks': 'vendor/angular-mocks/angular-mocks.js', + "bootstrap": "vendor/bootstrap/bootstrap.js", + 'angular-route': 'vendor/angular-route/angular-route.js', + 'angular-sanitize': 'vendor/angular-sanitize/angular-sanitize.js', + "angular-ui": "vendor/angular-ui/ui-bootstrap-tpls.js", + "angular-strap": "vendor/angular-other/angular-strap.js", + "angular-dragdrop": "vendor/angular-native-dragdrop/draganddrop.js", + "angular-bindonce": "vendor/angular-bindonce/bindonce.js", + "spectrum": "vendor/spectrum.js", + "bootstrap-tagsinput": "vendor/tagsinput/bootstrap-tagsinput.js", + "jquery.flot": "vendor/flot/jquery.flot", + "jquery.flot.pie": "vendor/flot/jquery.flot.pie", + "jquery.flot.selection": "vendor/flot/jquery.flot.selection", + "jquery.flot.stack": "vendor/flot/jquery.flot.stack", + "jquery.flot.stackpercent": "vendor/flot/jquery.flot.stackpercent", + "jquery.flot.time": "vendor/flot/jquery.flot.time", + "jquery.flot.crosshair": "vendor/flot/jquery.flot.crosshair", + "jquery.flot.fillbelow": "vendor/flot/jquery.flot.fillbelow", + "jquery.flot.gauge": "vendor/flot/jquery.flot.gauge", + "d3": "vendor/d3/d3.js", + "jquery.flot.dashes": "vendor/flot/jquery.flot.dashes", + "twemoji": "vendor/npm/twemoji/2/twemoji.amd.js", + "ace": "vendor/npm/ace-builds/src-noconflict/ace", + }, + + packages: { + app: { + defaultExtension: 'js', + }, + vendor: { + defaultExtension: 'js', + }, + }, + + map: { + }, + + meta: { + 'vendor/angular/angular.js': { + format: 'global', + deps: ['jquery'], + exports: 'angular', + }, + 'vendor/angular-mocks/angular-mocks.js': { + format: 'global', + deps: ['angular'], + }, + 'vendor/npm/eventemitter3/index.js': { + format: 'cjs', + exports: 'EventEmitter' + }, + 'vendor/npm/mousetrap/mousetrap.js': { + format: 'global', + exports: 'Mousetrap' + }, + 'vendor/npm/ace-builds/src-noconflict/ace.js': { + format: 'global', + exports: 'ace' + }, + } + }); + + function file2moduleName(filePath) { + return filePath.replace(/\\/g, '/') + .replace(/^\/base\//, '') + .replace(/\.\w*$/, ''); + } + + function onlySpecFiles(path) { + return /specs.*/.test(path); + } + + window.grafanaBootData = {settings: {}}; + + var modules = ['angular', 'angular-mocks', 'app/app']; + var promises = modules.map(function(name) { + return System.import(name); + }); + + Promise.all(promises).then(function(deps) { + var angular = deps[0]; + + angular.module('grafana', ['ngRoute']); + angular.module('grafana.services', ['ngRoute', '$strap.directives']); + angular.module('grafana.panels', []); + angular.module('grafana.controllers', []); + angular.module('grafana.directives', []); + angular.module('grafana.filters', []); + angular.module('grafana.routes', ['ngRoute']); + + // load specs + return Promise.all( + Object.keys(window.__karma__.files) // All files served by Karma. + .filter(onlySpecFiles) + .map(file2moduleName) + .map(function(path) { + // console.log(path); + return System.import(path); + })); + }).then(function() { + window.__karma__.start(); + }, function(error) { + window.__karma__.error(error.stack || error); + }).catch(function(error) { + window.__karma__.error(error.stack || error); + }); + +})(); diff --git a/public/vendor/flot/jquery.flot.js b/public/vendor/flot/jquery.flot.js index 2f1b60b0830..41f5ea2fd2d 100644 --- a/public/vendor/flot/jquery.flot.js +++ b/public/vendor/flot/jquery.flot.js @@ -602,6 +602,7 @@ Licensed under the MIT license. tickColor: null, // color for the ticks, e.g. "rgba(0,0,0,0.15)" margin: 0, // distance from the canvas edge to the grid labelMargin: 5, // in pixels + eventSectionHeight: 0, // space for event section axisMargin: 8, // in pixels borderWidth: 2, // in pixels minBorderMargin: null, // in pixels, null means taken from points radius @@ -1450,6 +1451,7 @@ Licensed under the MIT license. tickLength = axis.options.tickLength, axisMargin = options.grid.axisMargin, padding = options.grid.labelMargin, + eventSectionPadding = options.grid.eventSectionHeight, innermost = true, outermost = true, first = true, @@ -1490,7 +1492,9 @@ Licensed under the MIT license. padding += +tickLength; if (isXAxis) { + // Add space for event section lh += padding; + lh += eventSectionPadding; if (pos == "bottom") { plotOffset.bottom += lh + axisMargin; @@ -1518,6 +1522,7 @@ Licensed under the MIT license. axis.position = pos; axis.tickLength = tickLength; axis.box.padding = padding; + axis.box.eventSectionPadding = eventSectionPadding; axis.innermost = innermost; } @@ -2225,7 +2230,7 @@ Licensed under the MIT license. halign = "center"; x = plotOffset.left + axis.p2c(tick.v); if (axis.position == "bottom") { - y = box.top + box.padding; + y = box.top + box.padding + box.eventSectionPadding; } else { y = box.top + box.height - box.padding; valign = "bottom"; diff --git a/public/vendor/tagsinput/bootstrap-tagsinput.js b/public/vendor/tagsinput/bootstrap-tagsinput.js index 702b6416962..4bc29ecbbdb 100644 --- a/public/vendor/tagsinput/bootstrap-tagsinput.js +++ b/public/vendor/tagsinput/bootstrap-tagsinput.js @@ -28,15 +28,14 @@ this.$element = $(element); this.$element.hide(); + this.widthClass = options.widthClass || 'width-9'; this.isSelect = (element.tagName === 'SELECT'); this.multiple = (this.isSelect && element.hasAttribute('multiple')); this.objectItems = options && options.itemValue; this.placeholderText = element.hasAttribute('placeholder') ? this.$element.attr('placeholder') : ''; - this.inputSize = Math.max(1, this.placeholderText.length); this.$container = $('
      '); - this.$input = $('').appendTo(this.$container); + this.$input = $('').appendTo(this.$container); this.$element.after(this.$container); @@ -292,6 +291,13 @@ self.$input.focus(); }, self)); + self.$container.on('blur', 'input', $.proxy(function(event) { + var $input = $(event.target); + self.add($input.val()); + $input.val(''); + event.preventDefault(); + }, self)); + self.$container.on('keydown', 'input', $.proxy(function(event) { var $input = $(event.target), $inputWrapper = self.findInputWrapper(); diff --git a/scripts/webpack/webpack.common.js b/scripts/webpack/webpack.common.js index fd1114e8bbf..736369a4845 100644 --- a/scripts/webpack/webpack.common.js +++ b/scripts/webpack/webpack.common.js @@ -29,13 +29,14 @@ module.exports = { module: { rules: [ { - test: /\.(ts|tsx)$/, + test: /\.tsx?$/, enforce: 'pre', exclude: /node_modules/, use: { loader: 'tslint-loader', options: { - emitErrors: true + emitErrors: true, + typeCheck: false, } } }, @@ -59,10 +60,6 @@ module.exports = { } ] }, - // { - // test : /\.(ico|png|cur|jpg|ttf|eot|svg|woff(2)?)(\?[a-z0-9]+)?$/, - // loader : 'file-loader', - // }, { test: /\.html$/, exclude: /index\.template.html/, diff --git a/tasks/options/copy.js b/tasks/options/copy.js new file mode 100644 index 00000000000..1ef32af6951 --- /dev/null +++ b/tasks/options/copy.js @@ -0,0 +1,45 @@ +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/tsconfig.json b/tsconfig.json index 3864befd0fd..bc9222ac87d 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -9,8 +9,8 @@ "module": "esnext", "declaration": false, "allowSyntheticDefaultImports": true, - "inlineSourceMap": true, - "sourceMap": false, + "inlineSourceMap": false, + "sourceMap": true, "noEmitOnError": false, "emitDecoratorMetadata": false, "experimentalDecorators": false, @@ -28,7 +28,5 @@ "public/app/**/*.ts", "public/app/**/*.tsx", "public/test/**/*.ts" - ], - "exclude": [ ] } diff --git a/tslint.json b/tslint.json index 1f74b95bfdd..2e93d80ad25 100644 --- a/tslint.json +++ b/tslint.json @@ -6,7 +6,7 @@ "no-unused-variable": true, "curly": true, "class-name": true, - "semicolon": ["always"], + "semicolon": [true, "always", "ignore-bound-class-methods"], "triple-equals": [true, "allow-null-check"], "comment-format": [false, "check-space"], "eofline": true, @@ -26,7 +26,6 @@ ], "no-construct": true, "no-debugger": true, - "no-duplicate-variable": true, "no-empty": false, "no-eval": true, "no-inferrable-types": true, From 3519d8d69b52794565456442f6576d872171bc66 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Sat, 7 Oct 2017 12:23:20 +0200 Subject: [PATCH 74/83] annotations: minor change to default/edit annotation color --- public/app/core/utils/colors.ts | 4 ++ .../app/features/annotations/event_manager.ts | 40 +++++++++---------- public/app/features/dashboard/model.ts | 3 +- 3 files changed, 26 insertions(+), 21 deletions(-) diff --git a/public/app/core/utils/colors.ts b/public/app/core/utils/colors.ts index b9dd164d635..96ce5df9fcd 100644 --- a/public/app/core/utils/colors.ts +++ b/public/app/core/utils/colors.ts @@ -5,6 +5,10 @@ declare var tinycolor; export const PALETTE_ROWS = 4; export const PALETTE_COLUMNS = 14; +export const DEFAULT_ANNOTATION_COLOR = 'rgba(0, 211, 255, 1)'; +export const OK_COLOR = "rgba(11, 237, 50, 1)"; +export const ALERTING_COLOR = "rgba(237, 46, 24, 1)"; +export const NO_DATA_COLOR = "rgba(150, 150, 150, 1)"; let colors = [ "#7EB26D","#EAB839","#6ED0E0","#EF843C","#E24D42","#1F78C1","#BA43A9","#705DA0", diff --git a/public/app/features/annotations/event_manager.ts b/public/app/features/annotations/event_manager.ts index caa367e42f1..5f4534bacb1 100644 --- a/public/app/features/annotations/event_manager.ts +++ b/public/app/features/annotations/event_manager.ts @@ -2,18 +2,13 @@ import _ from 'lodash'; import moment from 'moment'; import {MetricsPanelCtrl} from 'app/plugins/sdk'; import {AnnotationEvent} from './event'; - -const OK_COLOR = "rgba(11, 237, 50, 1)", - ALERTING_COLOR = "rgba(237, 46, 24, 1)", - NO_DATA_COLOR = "rgba(150, 150, 150, 1)"; - +import {OK_COLOR, ALERTING_COLOR, NO_DATA_COLOR, DEFAULT_ANNOTATION_COLOR} from 'app/core/utils/colors'; export class EventManager { event: AnnotationEvent; editorOpen: boolean; - constructor(private panelCtrl: MetricsPanelCtrl) { - } + constructor(private panelCtrl: MetricsPanelCtrl) {} editorClosed() { this.event = null; @@ -54,21 +49,26 @@ export class EventManager { } var types = { - '$__alerting': { + $__alerting: { color: ALERTING_COLOR, position: 'BOTTOM', markerSize: 5, }, - '$__ok': { + $__ok: { color: OK_COLOR, position: 'BOTTOM', markerSize: 5, }, - '$__no_data': { + $__no_data: { color: NO_DATA_COLOR, position: 'BOTTOM', markerSize: 5, }, + $__editing: { + color: DEFAULT_ANNOTATION_COLOR, + position: 'BOTTOM', + markerSize: 5, + }, }; if (this.event) { @@ -79,9 +79,9 @@ export class EventManager { min: this.event.time.valueOf(), timeEnd: this.event.timeEnd.valueOf(), text: this.event.text, - eventType: '$__alerting', + eventType: '$__editing', editModel: this.event, - } + }, ]; } else { annotations = [ @@ -89,8 +89,8 @@ export class EventManager { min: this.event.time.valueOf(), text: this.event.text, editModel: this.event, - eventType: '$__alerting', - } + eventType: '$__editing', + }, ]; } } else { @@ -130,7 +130,7 @@ export class EventManager { levels: _.keys(types).length + 1, data: annotations, types: types, - manager: this + manager: this, }; } } @@ -141,7 +141,7 @@ function getRegions(events) { function addRegionMarking(regions, flotOptions) { let markings = flotOptions.grid.markings; - let defaultColor = 'rgb(237, 46, 24)'; + let defaultColor = DEFAULT_ANNOTATION_COLOR; let fillColor; _.each(regions, region => { @@ -158,8 +158,8 @@ function addRegionMarking(regions, flotOptions) { fillColor = convertToRGB(fillColor); } - fillColor = addAlphaToRGB(fillColor, 0.090); - markings.push({ xaxis: { from: region.min, to: region.timeEnd }, color: fillColor }); + fillColor = addAlphaToRGB(fillColor, 0.09); + markings.push({xaxis: {from: region.min, to: region.timeEnd}, color: fillColor}); }); } @@ -179,8 +179,8 @@ function convertToRGB(hex: string): string { let rgb = _.map(match.slice(1), hex_val => { return parseInt(hex_val, 16); }); - return 'rgb(' + rgb.join(',') + ')'; + return 'rgb(' + rgb.join(',') + ')'; } else { - return ""; + return ''; } } diff --git a/public/app/features/dashboard/model.ts b/public/app/features/dashboard/model.ts index 11067d3c68b..7c4cdfac59e 100644 --- a/public/app/features/dashboard/model.ts +++ b/public/app/features/dashboard/model.ts @@ -5,6 +5,7 @@ import moment from 'moment'; import _ from 'lodash'; import $ from 'jquery'; +import {DEFAULT_ANNOTATION_COLOR} from 'app/core/utils/colors'; import {Emitter, contextSrv, appEvents} from 'app/core/core'; import {DashboardRow} from './row/row_model'; import sortByKeys from 'app/core/utils/sort_by_keys'; @@ -93,7 +94,7 @@ export class DashboardModel { datasource: '-- Grafana --', name: 'Annotations & Alerts', type: 'dashboard', - iconColor: 'rgb(0, 211, 255)', + iconColor: DEFAULT_ANNOTATION_COLOR, enable: true, hide: true, builtIn: 1, From b228c23dbe4967125d449fd717b8c4f0216db9ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Sat, 7 Oct 2017 22:05:31 +0200 Subject: [PATCH 75/83] testing: fixing tests --- package.json | 8 +- .../dashboard/specs/history_srv_specs.ts | 50 +++--- .../datasource/prometheus/completer.ts | 6 +- .../prometheus/specs/completer_specs.ts | 170 +++++++++--------- public/test/specs/helpers.js | 3 +- scripts/webpack/webpack.dev.js | 2 +- yarn.lock | 129 ++++--------- 7 files changed, 154 insertions(+), 214 deletions(-) diff --git a/package.json b/package.json index 1d8cc8bf64c..bb4c64b3789 100644 --- a/package.json +++ b/package.json @@ -66,7 +66,7 @@ "karma-webpack": "^2.0.4", "lint-staged": "^4.2.3", "load-grunt-tasks": "3.5.2", - "mocha": "3.5.0", + "mocha": "^4.0.1", "ng-annotate-loader": "^0.6.1", "ng-annotate-webpack-plugin": "^0.2.1-pre", "ngtemplate-loader": "^2.0.1", @@ -87,8 +87,8 @@ "tslint-loader": "^3.5.3", "typescript": "^2.5.2", "webpack": "^3.6.0", - "webpack-cleanup-plugin": "^0.5.1", "webpack-bundle-analyzer": "^2.9.0", + "webpack-cleanup-plugin": "^0.5.1", "webpack-merge": "^4.1.0", "zone.js": "^0.7.2" }, @@ -97,7 +97,7 @@ "watch": "./node_modules/.bin/webpack --progress --colors --watch --config scripts/webpack/webpack.dev.js", "build": "./node_modules/.bin/grunt build", "test": "./node_modules/.bin/grunt test", - "lint" : "./node_modules/.bin/tslint -c tslint.json --project tsconfig.json --type-check", + "lint": "./node_modules/.bin/tslint -c tslint.json --project tsconfig.json --type-check", "watch-test": "./node_modules/grunt-cli/bin/grunt karma:dev" }, "license": "Apache-2.0", @@ -119,9 +119,9 @@ "mousetrap": "^1.6.0", "ngreact": "^0.4.1", "react": "^16.0.0", - "rxjs": "^5.4.3", "react-dom": "^16.0.0", "remarkable": "^1.7.1", + "rxjs": "^5.4.3", "tether": "^1.4.0", "tether-drop": "https://github.com/torkelo/drop" } diff --git a/public/app/features/dashboard/specs/history_srv_specs.ts b/public/app/features/dashboard/specs/history_srv_specs.ts index 621e91c8f87..354f41e4a15 100644 --- a/public/app/features/dashboard/specs/history_srv_specs.ts +++ b/public/app/features/dashboard/specs/history_srv_specs.ts @@ -21,50 +21,48 @@ describe('historySrv', function() { return [200, restoreResponse(parsedData.version)]; }); })); + beforeEach(ctx.createService('historySrv')); + function wrapPromise(ctx, angularPromise) { + return new Promise((resolve, reject) => { + angularPromise.then(resolve, reject); + ctx.$httpBackend.flush(); + }); + } + describe('getHistoryList', function() { - it('should return a versions array for the given dashboard id', function(done) { - ctx.service.getHistoryList({ id: 1 }).then(function(versions) { + it('should return a versions array for the given dashboard id', function() { + return wrapPromise(ctx, ctx.service.getHistoryList({ id: 1 }).then(function(versions) { expect(versions).to.eql(versionsResponse); - done(); - }); - ctx.$httpBackend.flush(); + })); }); - it('should return an empty array when not given an id', function(done) { - ctx.service.getHistoryList({ }).then(function(versions) { + it('should return an empty array when not given an id', function() { + return wrapPromise(ctx, ctx.service.getHistoryList({ }).then(function(versions) { expect(versions).to.eql([]); - done(); - }); - ctx.$httpBackend.flush(); + })); }); - it('should return an empty array when not given a dashboard', function(done) { - ctx.service.getHistoryList().then(function(versions) { + it('should return an empty array when not given a dashboard', function() { + return wrapPromise(ctx, ctx.service.getHistoryList().then(function(versions) { expect(versions).to.eql([]); - done(); - }); - ctx.$httpBackend.flush(); + })); }); }); describe('restoreDashboard', function() { - it('should return a success response given valid parameters', function(done) { - var version = 6; - ctx.service.restoreDashboard({ id: 1 }, version).then(function(response) { + it('should return a success response given valid parameters', function() { + let version = 6; + return wrapPromise(ctx, ctx.service.restoreDashboard({ id: 1 }, version).then(function(response) { expect(response).to.eql(restoreResponse(version)); - done(); - }); - ctx.$httpBackend.flush(); + })); }); - it('should return an empty object when not given an id', function(done) { - ctx.service.restoreDashboard({}, 6).then(function(response) { + it('should return an empty object when not given an id', function() { + return wrapPromise(ctx, ctx.service.restoreDashboard({}, 6).then(function(response) { expect(response).to.eql({}); - done(); - }); - ctx.$httpBackend.flush(); + })); }); }); }); diff --git a/public/app/plugins/datasource/prometheus/completer.ts b/public/app/plugins/datasource/prometheus/completer.ts index 4c4c86cb3e3..449a5f34b49 100644 --- a/public/app/plugins/datasource/prometheus/completer.ts +++ b/public/app/plugins/datasource/prometheus/completer.ts @@ -33,7 +33,7 @@ export class PromCompleter { return; } - this.getLabelNameAndValueForMetric(metricName).then(result => { + return this.getLabelNameAndValueForMetric(metricName).then(result => { var labelNames = this.transformToCompletions( _.uniq(_.flatten(result.map(r => { return Object.keys(r.metric); @@ -42,7 +42,6 @@ export class PromCompleter { this.labelNameCache[metricName] = labelNames; callback(null, labelNames); }); - return; case 'string.quoted': metricName = this.findMetricName(session, pos.row, pos.column); if (!metricName) { @@ -62,7 +61,7 @@ export class PromCompleter { return; } - this.getLabelNameAndValueForMetric(metricName).then(result => { + return this.getLabelNameAndValueForMetric(metricName).then(result => { var labelValues = this.transformToCompletions( _.uniq(result.map(r => { return r.metric[labelName]; @@ -72,7 +71,6 @@ export class PromCompleter { this.labelValueCache[metricName][labelName] = labelValues; callback(null, labelValues); }); - return; } if (prefix === '[') { diff --git a/public/app/plugins/datasource/prometheus/specs/completer_specs.ts b/public/app/plugins/datasource/prometheus/specs/completer_specs.ts index 5d160453e13..b8b5ef023f0 100644 --- a/public/app/plugins/datasource/prometheus/specs/completer_specs.ts +++ b/public/app/plugins/datasource/prometheus/specs/completer_specs.ts @@ -4,111 +4,115 @@ import {PromCompleter} from '../completer'; import {PrometheusDatasource} from '../datasource'; describe('Prometheus editor completer', function() { + function getSessionStub(data) { + return { + getTokenAt: sinon.stub().returns(data.currentToken), + getTokens: sinon.stub().returns(data.tokens), + getLine: sinon.stub().returns(data.line), + }; + } - let sessionData = { - currentToken: {}, - tokens: [], - line: '' - }; - let session = { - getTokenAt: sinon.stub().returns(sessionData.currentToken), - getTokens: sinon.stub().returns(sessionData.tokens), - getLine: sinon.stub().returns(sessionData.line), - }; - let editor = { session: session }; - + let editor = {}; let datasourceStub = { - performInstantQuery: sinon.stub().withArgs({ expr: '{__name__="node_cpu"' }).returns(Promise.resolve( - [ - { - metric: { - job: 'node', - instance: 'localhost:9100' - } - } - ] - )), - performSuggestQuery: sinon.stub().withArgs('node', true).returns(Promise.resolve( - [ - 'node_cpu' - ] - )) + performInstantQuery: sinon + .stub() + .withArgs({expr: '{__name__="node_cpu"'}) + .returns( + Promise.resolve({ + data: { + data: { + result: [ + { + metric: { + job: 'node', + instance: 'localhost:9100', + }, + }, + ], + }, + }, + }), + ), + performSuggestQuery: sinon + .stub() + .withArgs('node', true) + .returns(Promise.resolve(['node_cpu'])), }; + let completer = new PromCompleter(datasourceStub); - describe("When inside brackets", () => { - - it("Should return range vectors", () => { - completer.getCompletions(editor, session, { row: 0, column: 10 }, '[', (s, res) => { + describe('When inside brackets', () => { + it('Should return range vectors', () => { + const session = getSessionStub({ + currentToken: {}, + tokens: [], + line: '', + }); + completer.getCompletions(editor, session, {row: 0, column: 10}, '[', (s, res) => { expect(res[0]).to.eql({caption: '1s', value: '[1s', meta: 'range vector'}); }); }); - }); - describe("When inside label matcher, and located at label name", () => { - sessionData = { - currentToken: { type: 'entity.name.tag', value: 'j', index: 2, start: 9 }, - tokens: [ - { type: 'identifier', value: 'node_cpu' }, - { type: 'paren.lparen', value: '{' }, - { type: 'entity.name.tag', value: 'j', index: 2, start: 9 }, - { type: 'paren.rparen', value: '}' } - ], - line: 'node_cpu{j}' - }; + describe('When inside label matcher, and located at label name', () => { + it('Should return label name list', () => { + const session = getSessionStub({ + currentToken: {type: 'entity.name.tag', value: 'j', index: 2, start: 9}, + tokens: [ + {type: 'identifier', value: 'node_cpu'}, + {type: 'paren.lparen', value: '{'}, + {type: 'entity.name.tag', value: 'j', index: 2, start: 9}, + {type: 'paren.rparen', value: '}'}, + ], + line: 'node_cpu{j}', + }); - it("Should return label name list", () => { - completer.getCompletions(editor, session, { row: 0, column: 10 }, 'j', (s, res) => { - expect(res[0]).to.eql({caption: 'job', value: 'job', meta: 'label name'}); + return completer.getCompletions(editor, session, {row: 0, column: 10}, 'j', (s, res) => { + expect(res[0].meta).to.eql('label name'); }); }); - }); - describe("When inside label matcher, and located at label name with __name__ match", () => { - sessionData = { - currentToken: { type: 'entity.name.tag', value: 'j', index: 5, start: 22 }, - tokens: [ - { type: 'paren.lparen', value: '{' }, - { type: 'entity.name.tag', value: '__name__' }, - { type: 'keyword.operator', value: '=~' }, - { type: 'string.quoted', value: '"node_cpu"' }, - { type: 'punctuation.operator', value: ',' }, - { type: 'entity.name.tag', value: 'j', 'index': 5, 'start': 22 }, - { type: 'paren.rparen', value: '}' } - ], - line: '{__name__=~"node_cpu",j}' - }; + describe('When inside label matcher, and located at label name with __name__ match', () => { + it('Should return label name list', () => { + const session = getSessionStub({ + currentToken: {type: 'entity.name.tag', value: 'j', index: 5, start: 22}, + tokens: [ + {type: 'paren.lparen', value: '{'}, + {type: 'entity.name.tag', value: '__name__'}, + {type: 'keyword.operator', value: '=~'}, + {type: 'string.quoted', value: '"node_cpu"'}, + {type: 'punctuation.operator', value: ','}, + {type: 'entity.name.tag', value: 'j', index: 5, start: 22}, + {type: 'paren.rparen', value: '}'}, + ], + line: '{__name__=~"node_cpu",j}', + }); - it("Should return label name list", () => { - completer.getCompletions(editor, session, { row: 0, column: 23 }, 'j', (s, res) => { - expect(res[0]).to.eql({caption: 'job', value: 'job', meta: 'label name'}); + return completer.getCompletions(editor, session, {row: 0, column: 23}, 'j', (s, res) => { + expect(res[0].meta).to.eql('label name'); }); }); - }); - describe("When inside label matcher, and located at label value", () => { - sessionData = { - currentToken: { type: 'string.quoted', value: '"n"', index: 4, start: 13 }, - tokens: [ - { type: 'identifier', value: 'node_cpu' }, - { type: 'paren.lparen', value: '{' }, - { type: 'entity.name.tag', value: 'job' }, - { type: 'keyword.operator', value: '=' }, - { type: 'string.quoted', value: '"n"', index: 4, start: 13 }, - { type: 'paren.rparen', value: '}' } - ], - line: 'node_cpu{job="n"}' - }; + describe('When inside label matcher, and located at label value', () => { + it('Should return label value list', () => { + const session = getSessionStub({ + currentToken: {type: 'string.quoted', value: '"n"', index: 4, start: 13}, + tokens: [ + {type: 'identifier', value: 'node_cpu'}, + {type: 'paren.lparen', value: '{'}, + {type: 'entity.name.tag', value: 'job'}, + {type: 'keyword.operator', value: '='}, + {type: 'string.quoted', value: '"n"', index: 4, start: 13}, + {type: 'paren.rparen', value: '}'}, + ], + line: 'node_cpu{job="n"}', + }); - it("Should return label value list", () => { - completer.getCompletions(editor, session, { row: 0, column: 15 }, 'n', (s, res) => { - expect(res[0]).to.eql({caption: 'node', value: 'node', meta: 'label value'}); + return completer.getCompletions(editor, session, {row: 0, column: 15}, 'n', (s, res) => { + expect(res[0].meta).to.eql('label value'); }); }); - }); - }); diff --git a/public/test/specs/helpers.js b/public/test/specs/helpers.js index a091b1a3b70..40c4a75423c 100644 --- a/public/test/specs/helpers.js +++ b/public/test/specs/helpers.js @@ -103,7 +103,7 @@ define([ }; this.createService = function(name) { - return window.inject(function($q, $rootScope, $httpBackend, $injector, $location) { + return window.inject(function($q, $rootScope, $httpBackend, $injector, $location, $timeout) { self.$q = $q; self.$rootScope = $rootScope; self.$httpBackend = $httpBackend; @@ -111,6 +111,7 @@ define([ self.$rootScope.onAppEvent = function() {}; self.$rootScope.appEvent = function() {}; + self.$timeout = $timeout; self.service = $injector.get(name); }); diff --git a/scripts/webpack/webpack.dev.js b/scripts/webpack/webpack.dev.js index e852f966cf7..037212c26de 100644 --- a/scripts/webpack/webpack.dev.js +++ b/scripts/webpack/webpack.dev.js @@ -10,7 +10,7 @@ const WebpackCleanupPlugin = require('webpack-cleanup-plugin'); const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin; module.exports = merge(common, { - devtool: "source-map", + devtool: "eval-source-map", entry: { dark: './public/sass/grafana.dark.scss', diff --git a/yarn.lock b/yarn.lock index 32e25dd691e..5c47f42682e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1700,7 +1700,7 @@ combined-stream@^1.0.5, combined-stream@~1.0.5: dependencies: delayed-stream "~1.0.0" -commander@2.11.x, commander@~2.11.0: +commander@2.11.0, commander@2.11.x, commander@~2.11.0: version "2.11.0" resolved "https://registry.yarnpkg.com/commander/-/commander-2.11.0.tgz#157152fd1e7a6c8d98a5b715cf376df928004563" @@ -1710,7 +1710,7 @@ commander@2.8.x: dependencies: graceful-readlink ">= 1.0.0" -commander@2.9.0, commander@2.9.x, commander@^2.8.1, commander@^2.9.0, commander@~2.9.0: +commander@2.9.x, commander@^2.8.1, commander@^2.9.0, commander@~2.9.0: version "2.9.0" resolved "https://registry.yarnpkg.com/commander/-/commander-2.9.0.tgz#9c99094176e12240cb22d6c5146098400fe0f7d4" dependencies: @@ -2134,9 +2134,9 @@ debug@2.3.3: dependencies: ms "0.7.2" -debug@2.6.8: - version "2.6.8" - resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.8.tgz#e731531ca2ede27d188222427da17821d68ff4fc" +debug@3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261" dependencies: ms "2.0.0" @@ -2255,14 +2255,18 @@ di@^0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/di/-/di-0.0.1.tgz#806649326ceaa7caa3306d75d985ea2748ba913c" -diff@3.2.0, diff@^3.2.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/diff/-/diff-3.2.0.tgz#c9ce393a4b7cbd0b058a725c93df299027868ff9" +diff@3.3.1: + version "3.3.1" + resolved "https://registry.yarnpkg.com/diff/-/diff-3.3.1.tgz#aa8567a6eed03c531fc89d3f711cd0e5259dec75" diff@^2.0.2: version "2.2.3" resolved "https://registry.yarnpkg.com/diff/-/diff-2.2.3.tgz#60eafd0d28ee906e4e8ff0a52c1229521033bf99" +diff@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/diff/-/diff-3.2.0.tgz#c9ce393a4b7cbd0b058a725c93df299027868ff9" + diffie-hellman@^5.0.0: version "5.0.2" resolved "https://registry.yarnpkg.com/diffie-hellman/-/diffie-hellman-5.0.2.tgz#b5835739270cfe26acf632099fded2a07f209e5e" @@ -3352,14 +3356,14 @@ glob-parent@^2.0.0: dependencies: is-glob "^2.0.0" -glob@7.1.1: - version "7.1.1" - resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.1.tgz#805211df04faaf1c63a3600306cdf5ade50b2ec8" +glob@7.1.2, glob@^7.0.0, glob@^7.0.3, glob@^7.0.5, glob@^7.1.1, glob@^7.1.2, glob@~7.1.1, glob@~7.1.2: + version "7.1.2" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.2.tgz#c19c9df9a028702d678612384a6552404c636d15" dependencies: fs.realpath "^1.0.0" inflight "^1.0.4" inherits "2" - minimatch "^3.0.2" + minimatch "^3.0.4" once "^1.3.0" path-is-absolute "^1.0.0" @@ -3373,17 +3377,6 @@ glob@^5.0.1, glob@^5.0.15, glob@~5.0.0: once "^1.3.0" path-is-absolute "^1.0.0" -glob@^7.0.0, glob@^7.0.3, glob@^7.0.5, glob@^7.1.1, glob@^7.1.2, glob@~7.1.1, glob@~7.1.2: - version "7.1.2" - resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.2.tgz#c19c9df9a028702d678612384a6552404c636d15" - dependencies: - fs.realpath "^1.0.0" - inflight "^1.0.4" - inherits "2" - minimatch "^3.0.4" - once "^1.3.0" - path-is-absolute "^1.0.0" - glob@~7.0.0: version "7.0.6" resolved "https://registry.yarnpkg.com/glob/-/glob-7.0.6.tgz#211bafaf49e525b8cd93260d14ab136152b3f57a" @@ -3454,9 +3447,9 @@ graceful-fs@^4.1.0, graceful-fs@^4.1.11, graceful-fs@^4.1.2, graceful-fs@^4.1.6, version "1.0.1" resolved "https://registry.yarnpkg.com/graceful-readlink/-/graceful-readlink-1.0.1.tgz#4cafad76bc62f02fa039b2f94e9a3dd3a391a725" -growl@1.9.2: - version "1.9.2" - resolved "https://registry.yarnpkg.com/growl/-/growl-1.9.2.tgz#0ea7743715db8d8de2c5ede1775e1b45ac85c02f" +growl@1.10.3: + version "1.10.3" + resolved "https://registry.yarnpkg.com/growl/-/growl-1.10.3.tgz#1926ba90cf3edfe2adb4927f5880bc22c66c790f" grunt-angular-templates@^1.1.0: version "1.1.0" @@ -3772,7 +3765,7 @@ hawk@3.1.3, hawk@~3.1.3: hoek "2.x.x" sntp "1.x.x" -he@1.1.x: +he@1.1.1, he@1.1.x: version "1.1.1" resolved "https://registry.yarnpkg.com/he/-/he-1.1.1.tgz#93410fd21b009735151f8868c2f271f3427e23fd" @@ -4937,21 +4930,6 @@ lockfile@~1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/lockfile/-/lockfile-1.0.3.tgz#2638fc39a0331e9cac1a04b71799931c9c50df79" -lodash._baseassign@^3.0.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/lodash._baseassign/-/lodash._baseassign-3.2.0.tgz#8c38a099500f215ad09e59f1722fd0c52bfe0a4e" - dependencies: - lodash._basecopy "^3.0.0" - lodash.keys "^3.0.0" - -lodash._basecopy@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/lodash._basecopy/-/lodash._basecopy-3.0.1.tgz#8da0e6a876cf344c0ad8a54882111dd3c5c7ca36" - -lodash._basecreate@^3.0.0: - version "3.0.3" - resolved "https://registry.yarnpkg.com/lodash._basecreate/-/lodash._basecreate-3.0.3.tgz#1bc661614daa7fc311b7d03bf16806a0213cf821" - lodash._baseuniq@~4.6.0: version "4.6.0" resolved "https://registry.yarnpkg.com/lodash._baseuniq/-/lodash._baseuniq-4.6.0.tgz#0ebb44e456814af7905c6212fa2c9b2d51b841e8" @@ -4963,14 +4941,6 @@ lodash._createset@~4.0.0: version "4.0.3" resolved "https://registry.yarnpkg.com/lodash._createset/-/lodash._createset-4.0.3.tgz#0f4659fbb09d75194fa9e2b88a6644d363c9fe26" -lodash._getnative@^3.0.0: - version "3.9.1" - resolved "https://registry.yarnpkg.com/lodash._getnative/-/lodash._getnative-3.9.1.tgz#570bc7dede46d61cdcde687d65d3eecbaa3aaff5" - -lodash._isiterateecall@^3.0.0: - version "3.0.9" - resolved "https://registry.yarnpkg.com/lodash._isiterateecall/-/lodash._isiterateecall-3.0.9.tgz#5203ad7ba425fae842460e696db9cf3e6aac057c" - lodash._root@~3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/lodash._root/-/lodash._root-3.0.1.tgz#fba1c4524c19ee9a5f8136b4609f017cf4ded692" @@ -4991,38 +4961,14 @@ lodash.clonedeep@^4.3.2, lodash.clonedeep@~4.5.0: version "4.5.0" resolved "https://registry.yarnpkg.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz#e23f3f9c4f8fbdde872529c1071857a086e5ccef" -lodash.create@3.1.1: - version "3.1.1" - resolved "https://registry.yarnpkg.com/lodash.create/-/lodash.create-3.1.1.tgz#d7f2849f0dbda7e04682bb8cd72ab022461debe7" - dependencies: - lodash._baseassign "^3.0.0" - lodash._basecreate "^3.0.0" - lodash._isiterateecall "^3.0.0" - lodash.flattendeep@^4.4.0: version "4.4.0" resolved "https://registry.yarnpkg.com/lodash.flattendeep/-/lodash.flattendeep-4.4.0.tgz#fb030917f86a3134e5bc9bec0d69e0013ddfedb2" -lodash.isarguments@^3.0.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz#2f573d85c6a24289ff00663b491c1d338ff3458a" - -lodash.isarray@^3.0.0: - version "3.0.4" - resolved "https://registry.yarnpkg.com/lodash.isarray/-/lodash.isarray-3.0.4.tgz#79e4eb88c36a8122af86f844aa9bcd851b5fbb55" - lodash.kebabcase@^4.0.0: version "4.1.1" resolved "https://registry.yarnpkg.com/lodash.kebabcase/-/lodash.kebabcase-4.1.1.tgz#8489b1cb0d29ff88195cceca448ff6d6cc295c36" -lodash.keys@^3.0.0: - version "3.1.2" - resolved "https://registry.yarnpkg.com/lodash.keys/-/lodash.keys-3.1.2.tgz#4dbc0472b156be50a0b286855d1bd0b0c656098a" - dependencies: - lodash._getnative "^3.0.0" - lodash.isarguments "^3.0.0" - lodash.isarray "^3.0.0" - lodash.memoize@^4.1.2: version "4.1.2" resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe" @@ -5393,21 +5339,20 @@ mkdirp@0.5.1, mkdirp@0.5.x, mkdirp@0.x.x, "mkdirp@>=0.5 0", mkdirp@^0.5.0, mkdir dependencies: minimist "0.0.8" -mocha@3.5.0: - version "3.5.0" - resolved "https://registry.yarnpkg.com/mocha/-/mocha-3.5.0.tgz#1328567d2717f997030f8006234bce9b8cd72465" +mocha@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/mocha/-/mocha-4.0.1.tgz#0aee5a95cf69a4618820f5e51fa31717117daf1b" dependencies: browser-stdout "1.3.0" - commander "2.9.0" - debug "2.6.8" - diff "3.2.0" + commander "2.11.0" + debug "3.1.0" + diff "3.3.1" escape-string-regexp "1.0.5" - glob "7.1.1" - growl "1.9.2" - json3 "3.3.2" - lodash.create "3.1.1" + glob "7.1.2" + growl "1.10.3" + he "1.1.1" mkdirp "0.5.1" - supports-color "3.1.2" + supports-color "4.4.0" moment@^2.18.1: version "2.18.1" @@ -8010,11 +7955,11 @@ strip-json-comments@~2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a" -supports-color@3.1.2: - version "3.1.2" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-3.1.2.tgz#72a262894d9d408b956ca05ff37b2ed8a6e2a2d5" +supports-color@4.4.0, supports-color@^4.0.0, supports-color@^4.2.1, supports-color@^4.4.0: + version "4.4.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-4.4.0.tgz#883f7ddabc165142b2a61427f3352ded195d1a3e" dependencies: - has-flag "^1.0.0" + has-flag "^2.0.0" supports-color@^2.0.0: version "2.0.0" @@ -8026,12 +7971,6 @@ supports-color@^3.1.0, supports-color@^3.2.3: dependencies: has-flag "^1.0.0" -supports-color@^4.0.0, supports-color@^4.2.1, supports-color@^4.4.0: - version "4.4.0" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-4.4.0.tgz#883f7ddabc165142b2a61427f3352ded195d1a3e" - dependencies: - has-flag "^2.0.0" - svgo@^0.7.0: version "0.7.2" resolved "https://registry.yarnpkg.com/svgo/-/svgo-0.7.2.tgz#9f5772413952135c6fefbf40afe6a4faa88b4bb5" From 9e61cacd16c045c36b3f16d66163618fcde275c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Sun, 8 Oct 2017 15:56:42 +0200 Subject: [PATCH 76/83] tests: found the unhandled promise issue in the dash import tests --- .../app/features/dashboard/specs/dash_import_ctrl_specs.ts | 6 ++++-- public/test/index.ts | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/public/app/features/dashboard/specs/dash_import_ctrl_specs.ts b/public/app/features/dashboard/specs/dash_import_ctrl_specs.ts index 97983d60dc9..c541aca34b2 100644 --- a/public/app/features/dashboard/specs/dash_import_ctrl_specs.ts +++ b/public/app/features/dashboard/specs/dash_import_ctrl_specs.ts @@ -53,9 +53,10 @@ describe('DashImportCtrl', function() { // setup api mock backendSrv.get = sinon.spy(() => { return Promise.resolve({ + json: {} }); }); - ctx.ctrl.checkGnetDashboard(); + return ctx.ctrl.checkGnetDashboard(); }); it('should call gnet api with correct dashboard id', function() { @@ -69,9 +70,10 @@ describe('DashImportCtrl', function() { // setup api mock backendSrv.get = sinon.spy(() => { return Promise.resolve({ + json: {} }); }); - ctx.ctrl.checkGnetDashboard(); + return ctx.ctrl.checkGnetDashboard(); }); it('should call gnet api with correct dashboard id', function() { diff --git a/public/test/index.ts b/public/test/index.ts index 79c911cd32c..33f24331b67 100644 --- a/public/test/index.ts +++ b/public/test/index.ts @@ -21,7 +21,7 @@ angular.module('grafana.directives', []); angular.module('grafana.filters', []); angular.module('grafana.routes', ['ngRoute']); -const context = (require).context('../', true, /specs/); +const context = (require).context('../', true, /specs\.(tsx?|js)/); for (let key of context.keys()) { context(key); } From 08dc08550c34d8c38f89351acce0a4aea089edf4 Mon Sep 17 00:00:00 2001 From: Anton Hvornum Date: Sun, 8 Oct 2017 20:37:16 +0200 Subject: [PATCH 77/83] Corrected a PostgreSQL SELECT statement. (#9460) At least in my Postgresql 9.6.5, the old syntax of capitalized queries doesn't work (Linux).
      Running with UTF-8 as standard encoding and the ` notations didn't work either, so removed those. --- pkg/services/sqlstore/migrator/postgres_dialect.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/services/sqlstore/migrator/postgres_dialect.go b/pkg/services/sqlstore/migrator/postgres_dialect.go index 92109efdfab..8de26194411 100644 --- a/pkg/services/sqlstore/migrator/postgres_dialect.go +++ b/pkg/services/sqlstore/migrator/postgres_dialect.go @@ -104,7 +104,7 @@ func (db *Postgres) SqlType(c *Column) string { func (db *Postgres) TableCheckSql(tableName string) (string, []interface{}) { args := []interface{}{"grafana", tableName} - sql := "SELECT `TABLE_NAME` from `INFORMATION_SCHEMA`.`TABLES` WHERE `TABLE_SCHEMA`=? and `TABLE_NAME`=?" + sql := "SELECT table_name FROM information_schema.tables WHERE table_schema=? and table_name=?" return sql, args } From ec802dbc1fcdf8e56251ff55a6f7dde97c23106d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 9 Oct 2017 07:24:09 +0200 Subject: [PATCH 78/83] fix: escape metric segment auto complete, fixes #9423 --- public/app/core/directives/metric_segment.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/public/app/core/directives/metric_segment.js b/public/app/core/directives/metric_segment.js index 2e9442c15a0..37352556819 100644 --- a/public/app/core/directives/metric_segment.js +++ b/public/app/core/directives/metric_segment.js @@ -79,7 +79,9 @@ function (_, $, coreModule) { $scope.$apply(function() { $scope.getOptions({ $query: query }).then(function(altSegments) { $scope.altSegments = altSegments; - options = _.map($scope.altSegments, function(alt) { return alt.value; }); + options = _.map($scope.altSegments, function(alt) { + return _.escape(alt.value); + }); // add custom values if (segment.custom !== 'false') { From 5e4bbfb08e8512ce51b696fb3f195f1c9caf0dce Mon Sep 17 00:00:00 2001 From: bergquist Date: Fri, 6 Oct 2017 13:56:42 +0200 Subject: [PATCH 79/83] ignore pattern for vendored libs --- .gitignore | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.gitignore b/.gitignore index 2113bb2920b..fb43f933731 100644 --- a/.gitignore +++ b/.gitignore @@ -45,3 +45,10 @@ profile.cov /examples/*/dist /packaging/**/*.rpm /packaging/**/*.deb + +/vendor/**/*.py +/vendor/**/*.xml +/vendor/**/*.yml +/vendor/**/*_test.go +/vendor/**/.editorconfig +/vendor/**/appengine* From 8423260f58ede935eca8b8c780e212c2f45cb47e Mon Sep 17 00:00:00 2001 From: Daniel Lee Date: Mon, 9 Oct 2017 10:10:40 +0200 Subject: [PATCH 80/83] gzip: plugin readme content set explicitly Macaron's gzip middleware tries to automatically figure out the content type for a file when gzipped and seems to mostly fail with plugin readmes. This change sets the content type to plain text. Fixes #9344. Ref #5952. --- pkg/api/plugins.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pkg/api/plugins.go b/pkg/api/plugins.go index 042c03f9832..0483b624a30 100644 --- a/pkg/api/plugins.go +++ b/pkg/api/plugins.go @@ -158,7 +158,9 @@ func GetPluginMarkdown(c *middleware.Context) Response { return ApiError(500, "Could not get markdown file", err) } else { - return Respond(200, content) + resp := Respond(200, content) + resp.Header("Content-Type", "text/plain; charset=utf-8") + return resp } } From 6ce990af579facac7d7052447f5f82c05aa4c944 Mon Sep 17 00:00:00 2001 From: Axel Date: Mon, 9 Oct 2017 12:56:34 +0200 Subject: [PATCH 81/83] Show min interval query option for mixed datasource (#9467) --- public/app/plugins/datasource/mixed/plugin.json | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/public/app/plugins/datasource/mixed/plugin.json b/public/app/plugins/datasource/mixed/plugin.json index 658e24c5e03..b28c58710f0 100644 --- a/public/app/plugins/datasource/mixed/plugin.json +++ b/public/app/plugins/datasource/mixed/plugin.json @@ -5,5 +5,9 @@ "builtIn": true, "mixed": true, - "metrics": true + "metrics": true, + + "queryOptions": { + "minInterval": true + } } From a174ad4c9a590a9ce8c04ef7715f89412dbe9e63 Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Mon, 9 Oct 2017 14:54:14 +0300 Subject: [PATCH 82/83] Unified Color picker fixes (#9466) * colorpicker: fix opening error when color is undefined * colorpicker: replace spectrum picker by new color picker * colorpicker: remove old spectrum picker directive * annotations: use tinycolor for working with region colors --- package.json | 3 +- .../components/colorpicker/ColorPicker.tsx | 5 ++- .../colorpicker/ColorPickerPopover.tsx | 8 ++-- public/app/core/core.ts | 1 - public/app/core/directives/spectrum_picker.js | 41 ------------------- public/app/core/utils/colors.ts | 5 +-- .../app/features/annotations/editor_ctrl.ts | 6 +++ .../app/features/annotations/event_manager.ts | 36 ++++------------ .../features/annotations/partials/editor.html | 6 ++- .../plugins/panel/graph/thresholds_form.ts | 18 +++++++- .../app/plugins/panel/heatmap/heatmap_ctrl.ts | 7 ++++ .../heatmap/partials/display_editor.html | 2 +- .../app/plugins/panel/singlestat/editor.html | 14 ++----- public/app/plugins/panel/singlestat/module.ts | 13 ++++++ .../plugins/panel/table/column_options.html | 6 +-- .../app/plugins/panel/table/column_options.ts | 9 ++++ 16 files changed, 84 insertions(+), 96 deletions(-) delete mode 100644 public/app/core/directives/spectrum_picker.js diff --git a/package.json b/package.json index bb4c64b3789..bffbea9f547 100644 --- a/package.json +++ b/package.json @@ -123,6 +123,7 @@ "remarkable": "^1.7.1", "rxjs": "^5.4.3", "tether": "^1.4.0", - "tether-drop": "https://github.com/torkelo/drop" + "tether-drop": "https://github.com/torkelo/drop", + "tinycolor2": "^1.4.1" } } diff --git a/public/app/core/components/colorpicker/ColorPicker.tsx b/public/app/core/components/colorpicker/ColorPicker.tsx index 8ef51ce0be7..baf3f87cf81 100644 --- a/public/app/core/components/colorpicker/ColorPicker.tsx +++ b/public/app/core/components/colorpicker/ColorPicker.tsx @@ -77,5 +77,8 @@ export class ColorPicker extends React.Component { } coreModule.directive('colorPicker', function (reactDirective) { - return reactDirective(ColorPicker, ['color', 'onChange']); + return reactDirective(ColorPicker, [ + 'color', + ['onChange', { watchDepth: 'reference', wrapApply: true }] + ]); }); diff --git a/public/app/core/components/colorpicker/ColorPickerPopover.tsx b/public/app/core/components/colorpicker/ColorPickerPopover.tsx index 5c1d90d6046..09b6b8ec2c2 100644 --- a/public/app/core/components/colorpicker/ColorPickerPopover.tsx +++ b/public/app/core/components/colorpicker/ColorPickerPopover.tsx @@ -1,11 +1,11 @@ import React from 'react'; import $ from 'jquery'; +import tinycolor from 'tinycolor2'; import coreModule from 'app/core/core_module'; import { GfColorPalette } from './ColorPalette'; import { GfSpectrumPicker } from './SpectrumPicker'; -// Spectrum picker uses TinyColor and loads it as a global variable, so we can use it here also -declare var tinycolor; +const DEFAULT_COLOR = '#000000'; export interface IProps { color: string; @@ -19,8 +19,8 @@ export class ColorPickerPopover extends React.Component { super(props); this.state = { tab: 'palette', - color: this.props.color, - colorString: this.props.color + color: this.props.color || DEFAULT_COLOR, + colorString: this.props.color || DEFAULT_COLOR }; } diff --git a/public/app/core/core.ts b/public/app/core/core.ts index 95b2e20aab6..3b4fdd68611 100644 --- a/public/app/core/core.ts +++ b/public/app/core/core.ts @@ -5,7 +5,6 @@ import "./directives/dropdown_typeahead"; import "./directives/metric_segment"; import "./directives/misc"; import "./directives/ng_model_on_blur"; -import "./directives/spectrum_picker"; import "./directives/tags"; import "./directives/value_select_dropdown"; import "./directives/rebuild_on_change"; diff --git a/public/app/core/directives/spectrum_picker.js b/public/app/core/directives/spectrum_picker.js deleted file mode 100644 index 612188b71d9..00000000000 --- a/public/app/core/directives/spectrum_picker.js +++ /dev/null @@ -1,41 +0,0 @@ -define([ - 'angular', - '../core_module', - 'vendor/spectrum', -], -function (angular, coreModule) { - 'use strict'; - - coreModule.default.directive('spectrumPicker', function() { - return { - restrict: 'E', - require: 'ngModel', - scope: false, - replace: true, - template: "", - link: function(scope, element, attrs, ngModel) { - var input = element.find('input'); - var options = angular.extend({ - showAlpha: true, - showButtons: false, - color: ngModel.$viewValue, - change: function(color) { - scope.$apply(function() { - ngModel.$setViewValue(color.toRgbString()); - }); - } - }, scope.$eval(attrs.options)); - - ngModel.$render = function() { - input.spectrum('set', ngModel.$viewValue || ''); - }; - - input.spectrum(options); - - scope.$on('$destroy', function() { - input.spectrum('destroy'); - }); - } - }; - }); -}); diff --git a/public/app/core/utils/colors.ts b/public/app/core/utils/colors.ts index 96ce5df9fcd..a38c92a6476 100644 --- a/public/app/core/utils/colors.ts +++ b/public/app/core/utils/colors.ts @@ -1,7 +1,5 @@ import _ from 'lodash'; - -// Spectrum picker uses TinyColor and loads it as a global variable, so we can use it here also -declare var tinycolor; +import tinycolor from 'tinycolor2'; export const PALETTE_ROWS = 4; export const PALETTE_COLUMNS = 14; @@ -9,6 +7,7 @@ export const DEFAULT_ANNOTATION_COLOR = 'rgba(0, 211, 255, 1)'; export const OK_COLOR = "rgba(11, 237, 50, 1)"; 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 = [ "#7EB26D","#EAB839","#6ED0E0","#EF843C","#E24D42","#1F78C1","#BA43A9","#705DA0", diff --git a/public/app/features/annotations/editor_ctrl.ts b/public/app/features/annotations/editor_ctrl.ts index fafb51aea08..a52e241ce35 100644 --- a/public/app/features/annotations/editor_ctrl.ts +++ b/public/app/features/annotations/editor_ctrl.ts @@ -33,6 +33,8 @@ export class AnnotationsEditorCtrl { this.datasources = datasourceSrv.getAnnotationSources(); this.annotations = $scope.dashboard.annotations.list; this.reset(); + + this.onColorChange = this.onColorChange.bind(this); } datasourceChanged() { @@ -83,6 +85,10 @@ export class AnnotationsEditorCtrl { this.$scope.broadcastRefresh(); } + onColorChange(newColor) { + this.currentAnnotation.iconColor = newColor; + } + annotationEnabledChange() { this.$scope.broadcastRefresh(); } diff --git a/public/app/features/annotations/event_manager.ts b/public/app/features/annotations/event_manager.ts index 5f4534bacb1..e8ddd2d95d6 100644 --- a/public/app/features/annotations/event_manager.ts +++ b/public/app/features/annotations/event_manager.ts @@ -1,8 +1,9 @@ import _ from 'lodash'; import moment from 'moment'; +import tinycolor from 'tinycolor2'; import {MetricsPanelCtrl} from 'app/plugins/sdk'; import {AnnotationEvent} from './event'; -import {OK_COLOR, ALERTING_COLOR, NO_DATA_COLOR, DEFAULT_ANNOTATION_COLOR} from 'app/core/utils/colors'; +import {OK_COLOR, ALERTING_COLOR, NO_DATA_COLOR, DEFAULT_ANNOTATION_COLOR, REGION_FILL_ALPHA} from 'app/core/utils/colors'; export class EventManager { event: AnnotationEvent; @@ -151,36 +152,17 @@ function addRegionMarking(regions, flotOptions) { fillColor = defaultColor; } - // Convert #FFFFFF to rgb(255, 255, 255) - // because panels with alerting use this format - let hexPattern = /^#[\da-fA-f]{3,6}/; - if (hexPattern.test(fillColor)) { - fillColor = convertToRGB(fillColor); - } - - fillColor = addAlphaToRGB(fillColor, 0.09); + fillColor = addAlphaToRGB(fillColor, REGION_FILL_ALPHA); markings.push({xaxis: {from: region.min, to: region.timeEnd}, color: fillColor}); }); } -function addAlphaToRGB(rgb: string, alpha: number): string { - let rgbPattern = /^rgb\(/; - if (rgbPattern.test(rgb)) { - return rgb.replace(')', `, ${alpha})`).replace('rgb', 'rgba'); +function addAlphaToRGB(colorString: string, alpha: number): string { + let color = tinycolor(colorString); + if (color.isValid()) { + color.setAlpha(alpha); + return color.toRgbString(); } else { - return rgb.replace(/[\d\.]+\)/, `${alpha})`); - } -} - -function convertToRGB(hex: string): string { - let hexPattern = /#([\da-fA-F]{2})([\da-fA-F]{2})([\da-fA-F]{2})/g; - let match = hexPattern.exec(hex); - if (match) { - let rgb = _.map(match.slice(1), hex_val => { - return parseInt(hex_val, 16); - }); - return 'rgb(' + rgb.join(',') + ')'; - } else { - return ''; + return colorString; } } diff --git a/public/app/features/annotations/partials/editor.html b/public/app/features/annotations/partials/editor.html index e4d9c2c413f..4c0b8f7b127 100644 --- a/public/app/features/annotations/partials/editor.html +++ b/public/app/features/annotations/partials/editor.html @@ -119,8 +119,10 @@ label-class="width-7">
      - - + + + +
      diff --git a/public/app/plugins/panel/graph/thresholds_form.ts b/public/app/plugins/panel/graph/thresholds_form.ts index acf9c4e0030..c9f6a69c6b2 100644 --- a/public/app/plugins/panel/graph/thresholds_form.ts +++ b/public/app/plugins/panel/graph/thresholds_form.ts @@ -37,6 +37,20 @@ export class ThresholdFormCtrl { render() { this.panelCtrl.render(); } + + onFillColorChange(index) { + return (newColor) => { + this.panel.thresholds[index].fillColor = newColor; + this.render(); + }; + } + + onLineColorChange(index) { + return (newColor) => { + this.panel.thresholds[index].lineColor = newColor; + this.render(); + }; + } } var template = ` @@ -77,7 +91,7 @@ var template = `
      - +
      @@ -87,7 +101,7 @@ var template = `
      - +
      diff --git a/public/app/plugins/panel/heatmap/heatmap_ctrl.ts b/public/app/plugins/panel/heatmap/heatmap_ctrl.ts index b564339673f..10a5e1b3d4e 100644 --- a/public/app/plugins/panel/heatmap/heatmap_ctrl.ts +++ b/public/app/plugins/panel/heatmap/heatmap_ctrl.ts @@ -119,6 +119,8 @@ export class HeatmapCtrl extends MetricsPanelCtrl { this.events.on('data-error', this.onDataError.bind(this)); this.events.on('data-snapshot-load', this.onDataReceived.bind(this)); this.events.on('init-edit-mode', this.onInitEditMode.bind(this)); + + this.onCardColorChange = this.onCardColorChange.bind(this); } onInitEditMode() { @@ -236,6 +238,11 @@ export class HeatmapCtrl extends MetricsPanelCtrl { this.render(); } + onCardColorChange(newColor) { + this.panel.color.cardColor = newColor; + this.render(); + } + seriesHandler(seriesData) { let series = new TimeSeries({ datapoints: seriesData.datapoints, diff --git a/public/app/plugins/panel/heatmap/partials/display_editor.html b/public/app/plugins/panel/heatmap/partials/display_editor.html index f161bf6cab4..929cf1fe7d4 100644 --- a/public/app/plugins/panel/heatmap/partials/display_editor.html +++ b/public/app/plugins/panel/heatmap/partials/display_editor.html @@ -12,7 +12,7 @@
      - +
      diff --git a/public/app/plugins/panel/singlestat/editor.html b/public/app/plugins/panel/singlestat/editor.html index 1981b4357f8..f00d909d39c 100644 --- a/public/app/plugins/panel/singlestat/editor.html +++ b/public/app/plugins/panel/singlestat/editor.html @@ -68,14 +68,8 @@
      diff --git a/public/app/plugins/panel/singlestat/module.ts b/public/app/plugins/panel/singlestat/module.ts index e6e8858d6e8..92d03562557 100644 --- a/public/app/plugins/panel/singlestat/module.ts +++ b/public/app/plugins/panel/singlestat/module.ts @@ -92,6 +92,9 @@ class SingleStatCtrl extends MetricsPanelCtrl { this.events.on('data-error', this.onDataError.bind(this)); this.events.on('data-snapshot-load', this.onDataReceived.bind(this)); this.events.on('init-edit-mode', this.onInitEditMode.bind(this)); + + this.onSparklineColorChange = this.onSparklineColorChange.bind(this); + this.onSparklineFillChange = this.onSparklineFillChange.bind(this); } onInitEditMode() { @@ -221,6 +224,16 @@ class SingleStatCtrl extends MetricsPanelCtrl { }; } + onSparklineColorChange(newColor) { + this.panel.sparkline.lineColor = newColor; + this.render(); + } + + onSparklineFillChange(newColor) { + this.panel.sparkline.fillColor = newColor; + this.render(); + } + getDecimalsForValue(value) { if (_.isNumber(this.panel.decimals)) { return {decimals: this.panel.decimals, scaledDecimals: null}; diff --git a/public/app/plugins/panel/table/column_options.html b/public/app/plugins/panel/table/column_options.html index 3bd8ec24841..996f10960f9 100644 --- a/public/app/plugins/panel/table/column_options.html +++ b/public/app/plugins/panel/table/column_options.html @@ -80,13 +80,13 @@
      - + - + - +
      Invert diff --git a/public/app/plugins/panel/table/column_options.ts b/public/app/plugins/panel/table/column_options.ts index c95382804c3..23035293080 100644 --- a/public/app/plugins/panel/table/column_options.ts +++ b/public/app/plugins/panel/table/column_options.ts @@ -53,6 +53,8 @@ export class ColumnOptionsCtrl { return col.text; }); }; + + this.onColorChange = this.onColorChange.bind(this); } render() { @@ -104,6 +106,13 @@ export class ColumnOptionsCtrl { ref[2] = copy; this.panelCtrl.render(); } + + onColorChange(styleIndex, colorIndex) { + return (newColor) => { + this.panel.styles[styleIndex].colors[colorIndex] = newColor; + this.render(); + }; + } } /** @ngInject */ From 68829a821e0894ea329a6226afd4e59dfa07a29f Mon Sep 17 00:00:00 2001 From: Patrick O'Carroll Date: Mon, 9 Oct 2017 15:41:14 +0200 Subject: [PATCH 83/83] added insrtuctions for oauth2 okta bitbucket (#9471) --- docs/sources/installation/configuration.md | 34 ++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/docs/sources/installation/configuration.md b/docs/sources/installation/configuration.md index 370509f0a63..627a76a963e 100644 --- a/docs/sources/installation/configuration.md +++ b/docs/sources/installation/configuration.md @@ -454,6 +454,40 @@ allow_sign_up = true Set api_url to the resource that returns [OpenID UserInfo](https://connect2id.com/products/server/docs/api/userinfo) compatible information. +### Set up oauth2 with Okta + +First set up Grafana as an OpenId client "webapplication" in Okta. Then set the Base URIs to `https:///` and set the Login redirect URIs to `https:///login/generic_oauth`. + +Finaly set up the generic oauth module like this: +```bash +[auth.generic_oauth] +name = Okta +enabled = true +scopes = openid profile email +client_id = +client_secret = +auth_url = https:///oauth2/v1/authorize +token_url = https:///oauth2/v1/token +api_url = https:///oauth2/v1/userinfo +``` + +### Set up oauth2 with Bitbucket + +```bash +[auth.generic_oauth] +name = BitBucket +enabled = true +allow_sign_up = true +client_id = +client_secret = +scopes = account email +auth_url = https://bitbucket.org/site/oauth2/authorize +token_url = https://bitbucket.org/site/oauth2/access_token +api_url = https://api.bitbucket.org/2.0/user +team_ids = +allowed_organizations = +``` +
      ## [auth.basic]